Merge branch 'main' into mobile
This commit is contained in:
commit
6e4b39a8da
25 changed files with 1224 additions and 462 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -1,3 +1,15 @@
|
||||||
|
## 0.1.10
|
||||||
|
* feat: support single asterisk to italic by @Xazin in https://github.com/AppFlowy-IO/appflowy-editor/pull/92
|
||||||
|
* fix: remove platform check in theme on web by @Xazin in https://github.com/AppFlowy-IO/appflowy-editor/pull/91
|
||||||
|
* fix: improves cursor left word delete by @MayurSMahajan in https://github.com/AppFlowy-IO/appflowy-editor/pull/88
|
||||||
|
* fix: one overlay entry for selection menu by @Xazin in https://github.com/AppFlowy-IO/appflowy-editor/pull/82
|
||||||
|
* fix: changing heading from one level to another from toolbar by @LucasXu0 in https://github.com/AppFlowy-IO/appflowy-editor/pull/96
|
||||||
|
|
||||||
|
## 0.1.9
|
||||||
|
* feat: dark mode improvement by @hyj1204 in https://github.com/AppFlowy-IO/appflowy-editor/pull/90
|
||||||
|
* feat: shift+tab to outdent a indented bulletpoint or checkbox. by @MayurSMahajan in https://github.com/AppFlowy-IO/appflowy-editor/pull/63
|
||||||
|
* fix: enter to outdent checkbox/bullet lists by @Xazin in https://github.com/AppFlowy-IO/appflowy-editor/pull/84
|
||||||
|
|
||||||
## 0.1.8
|
## 0.1.8
|
||||||
* chore: minor updates to documentation.
|
* chore: minor updates to documentation.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1 @@
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="64px" height="64px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM15.36 14.3C15.65 14.59 15.65 15.07 15.36 15.36C15.21 15.51 15.02 15.58 14.83 15.58C14.64 15.58 14.45 15.51 14.3 15.36L12 13.06L9.7 15.36C9.55 15.51 9.36 15.58 9.17 15.58C8.98 15.58 8.79 15.51 8.64 15.36C8.35 15.07 8.35 14.59 8.64 14.3L10.94 12L8.64 9.7C8.35 9.41 8.35 8.93 8.64 8.64C8.93 8.35 9.41 8.35 9.7 8.64L12 10.94L14.3 8.64C14.59 8.35 15.07 8.35 15.36 8.64C15.65 8.93 15.65 9.41 15.36 9.7L13.06 12L15.36 14.3Z" fill="#BDBDBD"></path> </svg>
|
||||||
<circle cx="8" cy="8" r="7" fill="#F2F2F2"/>
|
|
||||||
<path d="M6 6L10 10" stroke="#BDBDBD" stroke-linecap="round"/>
|
|
||||||
<path d="M10 6L6 10" stroke="#BDBDBD" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 649 B |
|
|
@ -1,8 +1,14 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
/// Attributes is used to describe the Node's information.
|
/// Attributes is used to describe the Node's information.
|
||||||
///
|
///
|
||||||
/// Please note: The keywords in [BuiltInAttributeKey] are reserved.
|
/// Please note: The keywords in [BuiltInAttributeKey] are reserved.
|
||||||
typedef Attributes = Map<String, dynamic>;
|
typedef Attributes = Map<String, dynamic>;
|
||||||
|
|
||||||
|
bool isAttributesEqual(Attributes? a, Attributes? b) {
|
||||||
|
return mapEquals(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
Attributes? composeAttributes(
|
Attributes? composeAttributes(
|
||||||
Attributes? base,
|
Attributes? base,
|
||||||
Attributes? other, {
|
Attributes? other, {
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ export 'text_node_extensions.dart';
|
||||||
|
|
||||||
export 'text_style_extension.dart';
|
export 'text_style_extension.dart';
|
||||||
export 'url_launcher_extension.dart';
|
export 'url_launcher_extension.dart';
|
||||||
|
export 'position_extension.dart';
|
||||||
|
|
|
||||||
94
lib/src/extensions/position_extension.dart
Normal file
94
lib/src/extensions/position_extension.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
enum SelectionRange {
|
||||||
|
character,
|
||||||
|
word,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PositionExtension on Position {
|
||||||
|
Position? moveHorizontal(
|
||||||
|
EditorState editorState, {
|
||||||
|
bool moveLeft = true,
|
||||||
|
SelectionRange selectionRange = SelectionRange.character,
|
||||||
|
}) {
|
||||||
|
final node = editorState.document.nodeAtPath(path);
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moveLeft && offset == 0) {
|
||||||
|
final previousEnd = node.previous?.selectable?.end();
|
||||||
|
if (previousEnd != null) {
|
||||||
|
return previousEnd;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} else if (!moveLeft) {
|
||||||
|
final end = node.selectable?.end();
|
||||||
|
if (end != null && offset >= end.offset) {
|
||||||
|
return node.next?.selectable?.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (selectionRange) {
|
||||||
|
case SelectionRange.character:
|
||||||
|
if (node is TextNode) {
|
||||||
|
return Position(
|
||||||
|
path: path,
|
||||||
|
offset: moveLeft
|
||||||
|
? node.delta.prevRunePosition(offset)
|
||||||
|
: node.delta.nextRunePosition(offset),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Position(path: path, offset: offset);
|
||||||
|
case SelectionRange.word:
|
||||||
|
if (node is TextNode) {
|
||||||
|
final result = moveLeft
|
||||||
|
? node.selectable?.getWordBoundaryInPosition(
|
||||||
|
Position(
|
||||||
|
path: path,
|
||||||
|
offset: node.delta.prevRunePosition(offset),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: node.selectable?.getWordBoundaryInPosition(this);
|
||||||
|
if (result != null) {
|
||||||
|
return moveLeft ? result.start : result.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Position(path: path, offset: offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Position? moveVertical(
|
||||||
|
EditorState editorState, {
|
||||||
|
bool upwards = true,
|
||||||
|
}) {
|
||||||
|
final selection =
|
||||||
|
editorState.service.selectionService.currentSelection.value;
|
||||||
|
final rects = editorState.service.selectionService.selectionRects;
|
||||||
|
if (rects.isEmpty || selection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset offset;
|
||||||
|
if (selection.isBackward) {
|
||||||
|
final rect = rects.reduce(
|
||||||
|
(current, next) => current.bottom >= next.bottom ? current : next,
|
||||||
|
);
|
||||||
|
offset = upwards
|
||||||
|
? rect.topRight.translate(0, -rect.height)
|
||||||
|
: rect.bottomRight.translate(0, rect.height);
|
||||||
|
} else {
|
||||||
|
final rect = rects.reduce(
|
||||||
|
(current, next) => current.top <= next.top ? current : next,
|
||||||
|
);
|
||||||
|
offset = upwards
|
||||||
|
? rect.topLeft.translate(0, -rect.height)
|
||||||
|
: rect.bottomLeft.translate(0, rect.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return editorState.service.selectionService.getPositionInOffset(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -133,6 +133,11 @@ extension TextNodeExtension on TextNode {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isNotBulletOrCheckbox => ![
|
||||||
|
BuiltInAttributeKey.bulletedList,
|
||||||
|
BuiltInAttributeKey.checkbox
|
||||||
|
].contains(subtype);
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TextNodesExtension on List<TextNode> {
|
extension TextNodesExtension on List<TextNode> {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import 'package:appflowy_editor/src/editor_state.dart';
|
||||||
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||||
|
import 'package:appflowy_editor/src/render/style/editor_style.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ColorOption {
|
class ColorOption {
|
||||||
|
|
@ -19,6 +21,7 @@ enum _ColorType {
|
||||||
class ColorPicker extends StatefulWidget {
|
class ColorPicker extends StatefulWidget {
|
||||||
const ColorPicker({
|
const ColorPicker({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.editorState,
|
||||||
this.selectedFontColorHex,
|
this.selectedFontColorHex,
|
||||||
this.selectedBackgroundColorHex,
|
this.selectedBackgroundColorHex,
|
||||||
required this.pickerBackgroundColor,
|
required this.pickerBackgroundColor,
|
||||||
|
|
@ -29,7 +32,7 @@ class ColorPicker extends StatefulWidget {
|
||||||
required this.onSubmittedbackgroundColorHex,
|
required this.onSubmittedbackgroundColorHex,
|
||||||
required this.onSubmittedFontColorHex,
|
required this.onSubmittedFontColorHex,
|
||||||
});
|
});
|
||||||
|
final EditorState? editorState;
|
||||||
final String? selectedFontColorHex;
|
final String? selectedFontColorHex;
|
||||||
final String? selectedBackgroundColorHex;
|
final String? selectedBackgroundColorHex;
|
||||||
final Color pickerBackgroundColor;
|
final Color pickerBackgroundColor;
|
||||||
|
|
@ -53,6 +56,7 @@ class _ColorPickerState extends State<ColorPicker> {
|
||||||
TextEditingController();
|
TextEditingController();
|
||||||
final TextEditingController _backgroundColorOpacityController =
|
final TextEditingController _backgroundColorOpacityController =
|
||||||
TextEditingController();
|
TextEditingController();
|
||||||
|
EditorStyle? get style => widget.editorState?.editorStyle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -131,7 +135,6 @@ class _ColorPickerState extends State<ColorPicker> {
|
||||||
return Text(
|
return Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.grey,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -154,47 +157,48 @@ class _ColorPickerState extends State<ColorPicker> {
|
||||||
Widget _buildColorItem(_ColorType type, ColorOption option, bool isChecked) {
|
Widget _buildColorItem(_ColorType type, ColorOption option, bool isChecked) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 36,
|
height: 36,
|
||||||
child: InkWell(
|
child: TextButton.icon(
|
||||||
customBorder: RoundedRectangleBorder(
|
onPressed: () {
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
hoverColor: widget.pickerItemHoverColor,
|
|
||||||
onTap: () {
|
|
||||||
if (type == _ColorType.font) {
|
if (type == _ColorType.font) {
|
||||||
widget.onSubmittedFontColorHex(option.colorHex);
|
widget.onSubmittedFontColorHex(option.colorHex);
|
||||||
} else if (type == _ColorType.background) {
|
} else if (type == _ColorType.background) {
|
||||||
widget.onSubmittedbackgroundColorHex(option.colorHex);
|
widget.onSubmittedbackgroundColorHex(option.colorHex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Row(
|
icon: SizedBox.square(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
dimension: 12,
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Container(
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
// padding
|
color: Color(int.tryParse(option.colorHex) ?? 0xFFFFFFFF),
|
||||||
const SizedBox(width: 6),
|
shape: BoxShape.circle,
|
||||||
// icon
|
|
||||||
SizedBox.square(
|
|
||||||
dimension: 12,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(int.tryParse(option.colorHex) ?? 0xFFFFFFFF),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// padding
|
),
|
||||||
const SizedBox(width: 10),
|
),
|
||||||
// text
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
|
(Set<MaterialState> states) {
|
||||||
|
if (states.contains(MaterialState.hovered)) {
|
||||||
|
return style!.popupMenuHoverColor!;
|
||||||
|
}
|
||||||
|
return Colors.transparent;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
label: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
option.name,
|
option.name,
|
||||||
style:
|
style:
|
||||||
TextStyle(fontSize: 12, color: widget.pickerItemTextColor),
|
TextStyle(fontSize: 12, color: widget.pickerItemTextColor),
|
||||||
|
softWrap: false,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// checkbox
|
// checkbox
|
||||||
if (isChecked) const FlowySvg(name: 'checkmark'),
|
if (isChecked) const FlowySvg(name: 'checkmark'),
|
||||||
const SizedBox(width: 6),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -77,19 +77,19 @@ class _LinkMenuState extends State<LinkMenu> {
|
||||||
if (widget.linkText != null) ...[
|
if (widget.linkText != null) ...[
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
iconName: 'link',
|
iconName: 'link',
|
||||||
color: style?.selectionMenuItemIconColor,
|
color: style?.popupMenuFGColor,
|
||||||
text: 'Open link',
|
text: 'Open link',
|
||||||
onPressed: widget.onOpenLink,
|
onPressed: widget.onOpenLink,
|
||||||
),
|
),
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
iconName: 'copy',
|
iconName: 'copy',
|
||||||
color: style?.selectionMenuItemIconColor,
|
color: style?.popupMenuFGColor,
|
||||||
text: 'Copy link',
|
text: 'Copy link',
|
||||||
onPressed: widget.onCopyLink,
|
onPressed: widget.onCopyLink,
|
||||||
),
|
),
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
iconName: 'delete',
|
iconName: 'delete',
|
||||||
color: style?.selectionMenuItemIconColor,
|
color: style?.popupMenuFGColor,
|
||||||
text: 'Remove link',
|
text: 'Remove link',
|
||||||
onPressed: widget.onRemoveLink,
|
onPressed: widget.onRemoveLink,
|
||||||
),
|
),
|
||||||
|
|
@ -102,11 +102,10 @@ class _LinkMenuState extends State<LinkMenu> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader() {
|
Widget _buildHeader() {
|
||||||
return const Text(
|
return Text(
|
||||||
'Add your link',
|
'Add your link',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey,
|
fontSize: style?.textStyle?.fontSize,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -114,13 +113,17 @@ class _LinkMenuState extends State<LinkMenu> {
|
||||||
Widget _buildInput() {
|
Widget _buildInput() {
|
||||||
return TextField(
|
return TextField(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
style: const TextStyle(fontSize: 14.0),
|
style: TextStyle(
|
||||||
|
fontSize: style?.textStyle?.fontSize,
|
||||||
|
),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
controller: _textEditingController,
|
controller: _textEditingController,
|
||||||
onSubmitted: widget.onSubmitted,
|
onSubmitted: widget.onSubmitted,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'URL',
|
hintText: 'URL',
|
||||||
hintStyle: const TextStyle(fontSize: 14.0),
|
hintStyle: TextStyle(
|
||||||
|
fontSize: style?.textStyle?.fontSize,
|
||||||
|
),
|
||||||
contentPadding: const EdgeInsets.all(16.0),
|
contentPadding: const EdgeInsets.all(16.0),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
|
|
@ -151,20 +154,24 @@ class _LinkMenuState extends State<LinkMenu> {
|
||||||
name: iconName,
|
name: iconName,
|
||||||
color: color,
|
color: color,
|
||||||
),
|
),
|
||||||
style: TextButton.styleFrom(
|
|
||||||
minimumSize: const Size.fromHeight(40),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
),
|
|
||||||
label: Text(
|
label: Text(
|
||||||
text,
|
text,
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: style?.selectionMenuItemTextColor ?? Colors.black,
|
color: color,
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
|
(Set<MaterialState> states) {
|
||||||
|
if (states.contains(MaterialState.hovered)) {
|
||||||
|
return style!.popupMenuHoverColor!;
|
||||||
|
}
|
||||||
|
return Colors.transparent;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
||||||
final TextStyle? code;
|
final TextStyle? code;
|
||||||
final String? highlightColorHex;
|
final String? highlightColorHex;
|
||||||
|
|
||||||
|
// Item's pop up menu styles
|
||||||
|
final Color? popupMenuFGColor;
|
||||||
|
final Color? popupMenuHoverColor;
|
||||||
|
|
||||||
EditorStyle({
|
EditorStyle({
|
||||||
required this.padding,
|
required this.padding,
|
||||||
required this.backgroundColor,
|
required this.backgroundColor,
|
||||||
|
|
@ -66,6 +70,8 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
||||||
required this.code,
|
required this.code,
|
||||||
required this.highlightColorHex,
|
required this.highlightColorHex,
|
||||||
required this.lineHeight,
|
required this.lineHeight,
|
||||||
|
required this.popupMenuFGColor,
|
||||||
|
required this.popupMenuHoverColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -92,6 +98,8 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
||||||
TextStyle? code,
|
TextStyle? code,
|
||||||
String? highlightColorHex,
|
String? highlightColorHex,
|
||||||
double? lineHeight,
|
double? lineHeight,
|
||||||
|
Color? popupMenuFGColor,
|
||||||
|
Color? popupMenuHoverColor,
|
||||||
}) {
|
}) {
|
||||||
return EditorStyle(
|
return EditorStyle(
|
||||||
padding: padding ?? this.padding,
|
padding: padding ?? this.padding,
|
||||||
|
|
@ -123,6 +131,8 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
||||||
code: code ?? this.code,
|
code: code ?? this.code,
|
||||||
highlightColorHex: highlightColorHex ?? this.highlightColorHex,
|
highlightColorHex: highlightColorHex ?? this.highlightColorHex,
|
||||||
lineHeight: lineHeight ?? this.lineHeight,
|
lineHeight: lineHeight ?? this.lineHeight,
|
||||||
|
popupMenuFGColor: popupMenuFGColor ?? this.popupMenuFGColor,
|
||||||
|
popupMenuHoverColor: popupMenuHoverColor ?? this.popupMenuHoverColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,6 +193,9 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
||||||
code: TextStyle.lerp(code, other.code, t),
|
code: TextStyle.lerp(code, other.code, t),
|
||||||
highlightColorHex: highlightColorHex,
|
highlightColorHex: highlightColorHex,
|
||||||
lineHeight: lineHeight,
|
lineHeight: lineHeight,
|
||||||
|
popupMenuFGColor: Color.lerp(popupMenuFGColor, other.popupMenuFGColor, t),
|
||||||
|
popupMenuHoverColor:
|
||||||
|
Color.lerp(popupMenuHoverColor, other.popupMenuHoverColor, t),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,6 +236,8 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
||||||
),
|
),
|
||||||
highlightColorHex: '0x6000BCF0',
|
highlightColorHex: '0x6000BCF0',
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
|
popupMenuFGColor: const Color(0xFF333333),
|
||||||
|
popupMenuHoverColor: const Color(0xFFE0F8FF),
|
||||||
);
|
);
|
||||||
|
|
||||||
static final dark = light.copyWith(
|
static final dark = light.copyWith(
|
||||||
|
|
@ -240,5 +255,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
||||||
selectionMenuItemSelectedColor: const Color(0xFF00BCF0),
|
selectionMenuItemSelectedColor: const Color(0xFF00BCF0),
|
||||||
toolbarColor: const Color(0xFF131720),
|
toolbarColor: const Color(0xFF131720),
|
||||||
toolbarElevation: 0.0,
|
toolbarElevation: 0.0,
|
||||||
|
popupMenuFGColor: Colors.white,
|
||||||
|
popupMenuHoverColor: const Color(0xFF00BCF0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -364,8 +364,7 @@ bool _allSatisfy(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
OverlayEntry? _linkMenuOverlay;
|
OverlayEntry? _overlay;
|
||||||
OverlayEntry? _colorMenuOverlay;
|
|
||||||
|
|
||||||
EditorState? _editorState;
|
EditorState? _editorState;
|
||||||
bool _changeSelectionInner = false;
|
bool _changeSelectionInner = false;
|
||||||
|
|
@ -387,7 +386,7 @@ void showLinkMenu(
|
||||||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||||
matchRect = matchRect.shift(-baseOffset);
|
matchRect = matchRect.shift(-baseOffset);
|
||||||
|
|
||||||
_dismissLinkMenu();
|
_dismissOverlay();
|
||||||
_editorState = editorState;
|
_editorState = editorState;
|
||||||
|
|
||||||
// Since the link menu will only show in single text selection,
|
// Since the link menu will only show in single text selection,
|
||||||
|
|
@ -410,7 +409,7 @@ void showLinkMenu(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_linkMenuOverlay = OverlayEntry(
|
_overlay = OverlayEntry(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: matchRect.bottom + 5.0,
|
top: matchRect.bottom + 5.0,
|
||||||
|
|
@ -428,11 +427,11 @@ void showLinkMenu(
|
||||||
textNode: textNode,
|
textNode: textNode,
|
||||||
);
|
);
|
||||||
|
|
||||||
_dismissLinkMenu();
|
_dismissOverlay();
|
||||||
},
|
},
|
||||||
onCopyLink: () {
|
onCopyLink: () {
|
||||||
AppFlowyClipboard.setData(text: linkText);
|
AppFlowyClipboard.setData(text: linkText);
|
||||||
_dismissLinkMenu();
|
_dismissOverlay();
|
||||||
},
|
},
|
||||||
onRemoveLink: () {
|
onRemoveLink: () {
|
||||||
final transaction = editorState.transaction
|
final transaction = editorState.transaction
|
||||||
|
|
@ -443,7 +442,7 @@ void showLinkMenu(
|
||||||
{BuiltInAttributeKey.href: null},
|
{BuiltInAttributeKey.href: null},
|
||||||
);
|
);
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
_dismissLinkMenu();
|
_dismissOverlay();
|
||||||
},
|
},
|
||||||
onFocusChange: (value) {
|
onFocusChange: (value) {
|
||||||
if (value && customSelection != null) {
|
if (value && customSelection != null) {
|
||||||
|
|
@ -457,15 +456,15 @@ void showLinkMenu(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Overlay.of(context)?.insert(_linkMenuOverlay!);
|
Overlay.of(context)?.insert(_overlay!);
|
||||||
|
|
||||||
editorState.service.scrollService?.disable();
|
editorState.service.scrollService?.disable();
|
||||||
editorState.service.keyboardService?.disable();
|
editorState.service.keyboardService?.disable();
|
||||||
editorState.service.selectionService.currentSelection
|
editorState.service.selectionService.currentSelection
|
||||||
.addListener(_dismissLinkMenu);
|
.addListener(_dismissOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _dismissLinkMenu() {
|
void _dismissOverlay() {
|
||||||
// workaround: SelectionService has been released after hot reload.
|
// workaround: SelectionService has been released after hot reload.
|
||||||
final isSelectionDisposed =
|
final isSelectionDisposed =
|
||||||
_editorState?.service.selectionServiceKey.currentState == null;
|
_editorState?.service.selectionServiceKey.currentState == null;
|
||||||
|
|
@ -479,37 +478,13 @@ void _dismissLinkMenu() {
|
||||||
_changeSelectionInner = false;
|
_changeSelectionInner = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_linkMenuOverlay?.remove();
|
_overlay?.remove();
|
||||||
_linkMenuOverlay = null;
|
_overlay = null;
|
||||||
|
|
||||||
_editorState?.service.scrollService?.enable();
|
_editorState?.service.scrollService?.enable();
|
||||||
_editorState?.service.keyboardService?.enable();
|
_editorState?.service.keyboardService?.enable();
|
||||||
_editorState?.service.selectionService.currentSelection
|
_editorState?.service.selectionService.currentSelection
|
||||||
.removeListener(_dismissLinkMenu);
|
.removeListener(_dismissOverlay);
|
||||||
_editorState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _dismissColorMenu() {
|
|
||||||
// workaround: SelectionService has been released after hot reload.
|
|
||||||
final isSelectionDisposed =
|
|
||||||
_editorState?.service.selectionServiceKey.currentState == null;
|
|
||||||
if (isSelectionDisposed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_editorState?.service.selectionService.currentSelection.value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_changeSelectionInner) {
|
|
||||||
_changeSelectionInner = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_colorMenuOverlay?.remove();
|
|
||||||
_colorMenuOverlay = null;
|
|
||||||
|
|
||||||
_editorState?.service.scrollService?.enable();
|
|
||||||
_editorState?.service.keyboardService?.enable();
|
|
||||||
_editorState?.service.selectionService.currentSelection
|
|
||||||
.removeListener(_dismissColorMenu);
|
|
||||||
_editorState = null;
|
_editorState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -531,7 +506,7 @@ void showColorMenu(
|
||||||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||||
matchRect = matchRect.shift(-baseOffset);
|
matchRect = matchRect.shift(-baseOffset);
|
||||||
|
|
||||||
_dismissColorMenu();
|
_dismissOverlay();
|
||||||
_editorState = editorState;
|
_editorState = editorState;
|
||||||
|
|
||||||
// Since the link menu will only show in single text selection,
|
// Since the link menu will only show in single text selection,
|
||||||
|
|
@ -563,7 +538,7 @@ void showColorMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
final style = editorState.editorStyle;
|
final style = editorState.editorStyle;
|
||||||
_colorMenuOverlay = OverlayEntry(
|
_overlay = OverlayEntry(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: matchRect.bottom + 5.0,
|
top: matchRect.bottom + 5.0,
|
||||||
|
|
@ -571,10 +546,11 @@ void showColorMenu(
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: ColorPicker(
|
child: ColorPicker(
|
||||||
|
editorState: editorState,
|
||||||
pickerBackgroundColor:
|
pickerBackgroundColor:
|
||||||
style.selectionMenuBackgroundColor ?? Colors.white,
|
style.selectionMenuBackgroundColor ?? Colors.white,
|
||||||
pickerItemHoverColor: style.selectionMenuItemSelectedColor ??
|
pickerItemHoverColor:
|
||||||
Colors.blue.withOpacity(0.3),
|
style.popupMenuHoverColor ?? Colors.blue.withOpacity(0.3),
|
||||||
pickerItemTextColor:
|
pickerItemTextColor:
|
||||||
style.selectionMenuItemTextColor ?? Colors.black,
|
style.selectionMenuItemTextColor ?? Colors.black,
|
||||||
selectedFontColorHex: fontColorHex,
|
selectedFontColorHex: fontColorHex,
|
||||||
|
|
@ -587,26 +563,26 @@ void showColorMenu(
|
||||||
editorState,
|
editorState,
|
||||||
color,
|
color,
|
||||||
);
|
);
|
||||||
_dismissColorMenu();
|
_dismissOverlay();
|
||||||
},
|
},
|
||||||
onSubmittedFontColorHex: (color) {
|
onSubmittedFontColorHex: (color) {
|
||||||
formatFontColor(
|
formatFontColor(
|
||||||
editorState,
|
editorState,
|
||||||
color,
|
color,
|
||||||
);
|
);
|
||||||
_dismissColorMenu();
|
_dismissOverlay();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Overlay.of(context)?.insert(_colorMenuOverlay!);
|
Overlay.of(context)?.insert(_overlay!);
|
||||||
|
|
||||||
editorState.service.scrollService?.disable();
|
editorState.service.scrollService?.disable();
|
||||||
editorState.service.keyboardService?.disable();
|
editorState.service.keyboardService?.disable();
|
||||||
editorState.service.selectionService.currentSelection
|
editorState.service.selectionService.currentSelection
|
||||||
.addListener(_dismissColorMenu);
|
.addListener(_dismissOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ColorOption> _generateFontColorOptions(EditorState editorState) {
|
List<ColorOption> _generateFontColorOptions(EditorState editorState) {
|
||||||
|
|
|
||||||
|
|
@ -108,21 +108,21 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
|
||||||
|
|
||||||
for (final textNode in textNodes) {
|
for (final textNode in textNodes) {
|
||||||
var newAttributes = {...textNode.attributes};
|
var newAttributes = {...textNode.attributes};
|
||||||
for (final globalStyleKey in BuiltInAttributeKey.globalStyleKeys) {
|
if (isAttributesEqual(newAttributes, attributes)) {
|
||||||
if (newAttributes.keys.contains(globalStyleKey)) {
|
for (final key in attributes.keys) {
|
||||||
newAttributes[globalStyleKey] = null;
|
newAttributes[key] = null;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (final globalStyleKey in BuiltInAttributeKey.globalStyleKeys) {
|
||||||
|
if (newAttributes.keys.contains(globalStyleKey)) {
|
||||||
|
newAttributes[globalStyleKey] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if an attribute already exists in the node, it should be removed instead
|
||||||
|
newAttributes.addAll(attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if an attribute already exists in the node, it should be removed instead
|
|
||||||
for (final entry in attributes.entries) {
|
|
||||||
if (textNode.attributes.containsKey(entry.key) &&
|
|
||||||
textNode.attributes[entry.key] == entry.value) {
|
|
||||||
// attribute is not added to the node new attributes
|
|
||||||
} else {
|
|
||||||
newAttributes.addEntries([entry]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transaction
|
transaction
|
||||||
..updateNode(
|
..updateNode(
|
||||||
textNode,
|
textNode,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ ShortcutEventHandler cursorLeftSelect = (editorState, event) {
|
||||||
if (nodes.isEmpty || selection == null) {
|
if (nodes.isEmpty || selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
final end = selection.end.goLeft(editorState);
|
final end = selection.end.moveHorizontal(editorState);
|
||||||
if (end == null) {
|
if (end == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ ShortcutEventHandler cursorRightSelect = (editorState, event) {
|
||||||
if (nodes.isEmpty || selection == null) {
|
if (nodes.isEmpty || selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
final end = selection.end.goRight(editorState);
|
final end = selection.end.moveHorizontal(editorState, moveLeft: false);
|
||||||
if (end == null) {
|
if (end == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ ShortcutEventHandler cursorUpSelect = (editorState, event) {
|
||||||
if (nodes.isEmpty || selection == null) {
|
if (nodes.isEmpty || selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
final end = _moveVertical(editorState);
|
final end = selection.end.moveVertical(editorState);
|
||||||
if (end == null) {
|
if (end == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +55,7 @@ ShortcutEventHandler cursorDownSelect = (editorState, event) {
|
||||||
if (nodes.isEmpty || selection == null) {
|
if (nodes.isEmpty || selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
final end = _moveVertical(editorState, upwards: false);
|
final end = selection.end.moveVertical(editorState, upwards: false);
|
||||||
if (end == null) {
|
if (end == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +225,7 @@ ShortcutEventHandler cursorUp = (editorState, event) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final upPosition = _moveVertical(editorState);
|
final upPosition = selection.end.moveVertical(editorState);
|
||||||
editorState.updateCursorSelection(
|
editorState.updateCursorSelection(
|
||||||
upPosition == null ? null : Selection.collapsed(upPosition),
|
upPosition == null ? null : Selection.collapsed(upPosition),
|
||||||
);
|
);
|
||||||
|
|
@ -241,7 +241,7 @@ ShortcutEventHandler cursorDown = (editorState, event) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final downPosition = _moveVertical(editorState, upwards: false);
|
final downPosition = selection.end.moveVertical(editorState, upwards: false);
|
||||||
editorState.updateCursorSelection(
|
editorState.updateCursorSelection(
|
||||||
downPosition == null ? null : Selection.collapsed(downPosition),
|
downPosition == null ? null : Selection.collapsed(downPosition),
|
||||||
);
|
);
|
||||||
|
|
@ -258,7 +258,7 @@ ShortcutEventHandler cursorLeft = (editorState, event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Position newPosition = selection.isCollapsed
|
Position newPosition = selection.isCollapsed
|
||||||
? selection.start.goLeft(editorState) ?? selection.start
|
? selection.start.moveHorizontal(editorState) ?? selection.start
|
||||||
: selection.start;
|
: selection.start;
|
||||||
|
|
||||||
editorState.service.selectionService.updateSelection(
|
editorState.service.selectionService.updateSelection(
|
||||||
|
|
@ -277,7 +277,8 @@ ShortcutEventHandler cursorRight = (editorState, event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
final newPosition = selection.isCollapsed
|
final newPosition = selection.isCollapsed
|
||||||
? selection.start.goRight(editorState) ?? selection.end
|
? selection.start.moveHorizontal(editorState, moveLeft: false) ??
|
||||||
|
selection.end
|
||||||
: selection.end;
|
: selection.end;
|
||||||
|
|
||||||
editorState.service.selectionService.updateSelection(
|
editorState.service.selectionService.updateSelection(
|
||||||
|
|
@ -294,8 +295,10 @@ ShortcutEventHandler cursorLeftWordSelect = (editorState, event) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final end =
|
final end = selection.end.moveHorizontal(
|
||||||
selection.end.goLeft(editorState, selectionRange: _SelectionRange.word);
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
);
|
||||||
if (end == null) {
|
if (end == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
@ -316,8 +319,10 @@ ShortcutEventHandler cursorLeftWordMove = (editorState, event) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final newPosition = selection.start
|
final newPosition = selection.start.moveHorizontal(
|
||||||
.goLeft(editorState, selectionRange: _SelectionRange.word) ??
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
) ??
|
||||||
selection.start;
|
selection.start;
|
||||||
|
|
||||||
editorState.service.selectionService.updateSelection(
|
editorState.service.selectionService.updateSelection(
|
||||||
|
|
@ -336,8 +341,11 @@ ShortcutEventHandler cursorRightWordMove = (editorState, event) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final newPosition = selection.start
|
final newPosition = selection.start.moveHorizontal(
|
||||||
.goRight(editorState, selectionRange: _SelectionRange.word) ??
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
moveLeft: false,
|
||||||
|
) ??
|
||||||
selection.end;
|
selection.end;
|
||||||
|
|
||||||
editorState.service.selectionService.updateSelection(
|
editorState.service.selectionService.updateSelection(
|
||||||
|
|
@ -354,8 +362,11 @@ ShortcutEventHandler cursorRightWordSelect = (editorState, event) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final end =
|
final end = selection.end.moveHorizontal(
|
||||||
selection.end.goRight(editorState, selectionRange: _SelectionRange.word);
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
moveLeft: false,
|
||||||
|
);
|
||||||
if (end == null) {
|
if (end == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
@ -367,63 +378,6 @@ ShortcutEventHandler cursorRightWordSelect = (editorState, event) {
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
};
|
};
|
||||||
|
|
||||||
ShortcutEventHandler cursorLeftWordDelete = (editorState, event) {
|
|
||||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
|
||||||
.whereType<TextNode>();
|
|
||||||
final selection = editorState.service.selectionService.currentSelection.value;
|
|
||||||
|
|
||||||
if (textNodes.isEmpty || selection == null) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
final textNode = textNodes.first;
|
|
||||||
|
|
||||||
final startOfWord =
|
|
||||||
selection.end.goLeft(editorState, selectionRange: _SelectionRange.word);
|
|
||||||
|
|
||||||
if (startOfWord == null) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
final transaction = editorState.transaction;
|
|
||||||
transaction.deleteText(
|
|
||||||
textNode,
|
|
||||||
startOfWord.offset,
|
|
||||||
selection.end.offset - startOfWord.offset,
|
|
||||||
);
|
|
||||||
|
|
||||||
editorState.apply(transaction);
|
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
};
|
|
||||||
|
|
||||||
ShortcutEventHandler cursorLeftSentenceDelete = (editorState, event) {
|
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
|
||||||
final selection = editorState.service.selectionService.currentSelection.value;
|
|
||||||
if (nodes.isEmpty || selection == null) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodes.length == 1 && nodes.first is TextNode) {
|
|
||||||
final textNode = nodes.first as TextNode;
|
|
||||||
if (textNode.toPlainText().isEmpty) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selection.isCollapsed) {
|
|
||||||
final deleteTransaction = editorState.transaction;
|
|
||||||
deleteTransaction.deleteText(
|
|
||||||
nodes.first as TextNode,
|
|
||||||
0,
|
|
||||||
selection.end.offset,
|
|
||||||
);
|
|
||||||
editorState.apply(deleteTransaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum _SelectionRange {
|
enum _SelectionRange {
|
||||||
character,
|
character,
|
||||||
word,
|
word,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
ShortcutEventHandler cursorLeftWordDelete = (editorState, event) {
|
||||||
|
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
|
.whereType<TextNode>();
|
||||||
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
|
|
||||||
|
if (textNodes.isEmpty || selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final textNode = textNodes.first;
|
||||||
|
|
||||||
|
//we store the position where the current word starts.
|
||||||
|
var startOfWord = selection.end.moveHorizontal(
|
||||||
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (startOfWord == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
//check if the selected word is whitespace
|
||||||
|
final selectedWord = textNode
|
||||||
|
.toPlainText()
|
||||||
|
.substring(startOfWord.offset, selection.end.offset);
|
||||||
|
|
||||||
|
//if it is whitespace then we have to update the selection to include
|
||||||
|
//the left word from the whitespace.
|
||||||
|
if (selectedWord.trim().isEmpty) {
|
||||||
|
//make a new selection from the left of the whitespace.
|
||||||
|
final newSelection = Selection.single(
|
||||||
|
path: startOfWord.path,
|
||||||
|
startOffset: startOfWord.offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
//we need to check if this position is not null
|
||||||
|
final newStartOfWord = newSelection.end.moveHorizontal(
|
||||||
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
);
|
||||||
|
|
||||||
|
//this handles the edge case where the textNode only consists single space.
|
||||||
|
if (newStartOfWord != null) {
|
||||||
|
startOfWord = newStartOfWord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.deleteText(
|
||||||
|
textNode,
|
||||||
|
startOfWord.offset,
|
||||||
|
selection.end.offset - startOfWord.offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
editorState.apply(transaction);
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
ShortcutEventHandler cursorLeftSentenceDelete = (editorState, event) {
|
||||||
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
|
if (nodes.isEmpty || selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodes.length == 1 && nodes.first is TextNode) {
|
||||||
|
final textNode = nodes.first as TextNode;
|
||||||
|
if (textNode.toPlainText().isEmpty) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
final deleteTransaction = editorState.transaction;
|
||||||
|
deleteTransaction.deleteText(
|
||||||
|
nodes.first as TextNode,
|
||||||
|
0,
|
||||||
|
selection.end.offset,
|
||||||
|
);
|
||||||
|
editorState.apply(deleteTransaction, withUpdateSelection: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
@ -77,15 +77,27 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||||
// insert a empty text node before.
|
// insert a empty text node before.
|
||||||
if (selection.isCollapsed && selection.start.offset == 0) {
|
if (selection.isCollapsed && selection.start.offset == 0) {
|
||||||
if (textNode.toPlainText().isEmpty && textNode.subtype != null) {
|
if (textNode.toPlainText().isEmpty && textNode.subtype != null) {
|
||||||
|
final path =
|
||||||
|
textNode.path.length > 1 ? [++textNode.path.first] : textNode.path;
|
||||||
|
|
||||||
final afterSelection = Selection.collapsed(
|
final afterSelection = Selection.collapsed(
|
||||||
Position(path: textNode.path, offset: 0),
|
Position(path: path, offset: 0),
|
||||||
);
|
);
|
||||||
final transaction = editorState.transaction
|
|
||||||
..updateNode(textNode, {
|
final transaction = editorState.transaction;
|
||||||
BuiltInAttributeKey.subtype: null,
|
if (textNode.path.length > 1) {
|
||||||
textNode.subtype!: null,
|
transaction
|
||||||
})
|
..deleteNode(textNode)
|
||||||
..afterSelection = afterSelection;
|
..insertNode(path, textNode)
|
||||||
|
..afterSelection = afterSelection;
|
||||||
|
} else {
|
||||||
|
transaction
|
||||||
|
..updateNode(textNode, {
|
||||||
|
BuiltInAttributeKey.subtype: null,
|
||||||
|
textNode.subtype!: null,
|
||||||
|
})
|
||||||
|
..afterSelection = afterSelection;
|
||||||
|
}
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
|
|
||||||
final nextNode = textNode.next;
|
final nextNode = textNode.next;
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,17 @@ ShortcutEventHandler markdownLinkOrImageHandler = (editorState, event) {
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
};
|
};
|
||||||
|
|
||||||
ShortcutEventHandler underscoreToItalicHandler = (editorState, event) {
|
ShortcutEventHandler singleUnderscoreToItalicHandler = (editorState, event) {
|
||||||
|
return singleCharacterToItalicHandler(editorState, event, '_');
|
||||||
|
};
|
||||||
|
|
||||||
|
ShortcutEventHandler singleAsteriskToItalicHandler = (editorState, event) {
|
||||||
|
return singleCharacterToItalicHandler(editorState, event, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyEventResult Function(EditorState, RawKeyEvent?, String)
|
||||||
|
singleCharacterToItalicHandler =
|
||||||
|
(EditorState editorState, RawKeyEvent? event, String character) {
|
||||||
// Obtain the selection and selected nodes of the current document through the 'selectionService'
|
// Obtain the selection and selected nodes of the current document through the 'selectionService'
|
||||||
// to determine whether the selection is collapsed and whether the selected node is a text node.
|
// to determine whether the selection is collapsed and whether the selected node is a text node.
|
||||||
final selectionService = editorState.service.selectionService;
|
final selectionService = editorState.service.selectionService;
|
||||||
|
|
@ -284,24 +294,24 @@ ShortcutEventHandler underscoreToItalicHandler = (editorState, event) {
|
||||||
|
|
||||||
final textNode = textNodes.first;
|
final textNode = textNodes.first;
|
||||||
final text = textNode.toPlainText();
|
final text = textNode.toPlainText();
|
||||||
// Determine if an 'underscore' already exists in the text node and only once.
|
// Determine if a 'character' already exists in the text node and only once.
|
||||||
final firstUnderscore = text.indexOf('_');
|
final firstCharacter = text.indexOf(character);
|
||||||
final lastUnderscore = text.lastIndexOf('_');
|
final lastCharacter = text.lastIndexOf(character);
|
||||||
if (firstUnderscore == -1 ||
|
if (firstCharacter == -1 ||
|
||||||
firstUnderscore != lastUnderscore ||
|
firstCharacter != lastCharacter ||
|
||||||
firstUnderscore == selection.start.offset - 1) {
|
firstCharacter == selection.start.offset - 1) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the previous 'underscore',
|
// Delete the previous 'character',
|
||||||
// update the style of the text surrounded by the two underscores to 'italic',
|
// update the style of the text surrounded by the two characters to 'italic',
|
||||||
// and update the cursor position.
|
// and update the cursor position.
|
||||||
final transaction = editorState.transaction
|
final transaction = editorState.transaction
|
||||||
..deleteText(textNode, firstUnderscore, 1)
|
..deleteText(textNode, firstCharacter, 1)
|
||||||
..formatText(
|
..formatText(
|
||||||
textNode,
|
textNode,
|
||||||
firstUnderscore,
|
firstCharacter,
|
||||||
selection.end.offset - firstUnderscore - 1,
|
selection.end.offset - firstCharacter - 1,
|
||||||
{
|
{
|
||||||
BuiltInAttributeKey.italic: true,
|
BuiltInAttributeKey.italic: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
ShortcutEventHandler outdentTabHandler = (editorState, event) {
|
||||||
|
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;
|
||||||
|
var previous = textNode.previous;
|
||||||
|
|
||||||
|
if (textNode.isNotBulletOrCheckbox) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
//if the current node is having a path which is of size 1.
|
||||||
|
//for example [0], then that means, it is not indented
|
||||||
|
//thus we ignore this event.
|
||||||
|
final oldPath = textNode.path;
|
||||||
|
if (oldPath.length == 1) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
//we need to check if the previous node is a list type or not,
|
||||||
|
//but the previous getter gives the previous node in the LinkedList
|
||||||
|
//in case of nested bullet lists, where an element's path is [1,0]
|
||||||
|
//the previous getter will return null, but in this case: we should
|
||||||
|
//assign previous's path to be = [1]
|
||||||
|
if (previous == null) {
|
||||||
|
if (oldPath.last != 0) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final previousPath = oldPath.sublist(0, oldPath.length - 1);
|
||||||
|
editorState.updateCursorSelection(
|
||||||
|
Selection.single(path: previousPath, startOffset: 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
final prevSelection =
|
||||||
|
editorState.service.selectionService.currentSelection.value;
|
||||||
|
|
||||||
|
final prevNodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
|
.whereType<TextNode>();
|
||||||
|
|
||||||
|
if (prevNodes.length != 1 ||
|
||||||
|
prevSelection == null ||
|
||||||
|
!prevSelection.isSingle) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous = prevNodes.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textNode.isNotBulletOrCheckbox) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final path = oldPath.sublist(0, oldPath.length - 1)..last += 1;
|
||||||
|
|
||||||
|
final afterSelection = Selection(
|
||||||
|
start: selection.start.copyWith(path: path),
|
||||||
|
end: selection.end.copyWith(path: path),
|
||||||
|
);
|
||||||
|
final transaction = editorState.transaction
|
||||||
|
..deleteNode(textNode)
|
||||||
|
..insertNode(path, textNode)
|
||||||
|
..afterSelection = afterSelection;
|
||||||
|
editorState.apply(transaction);
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
@ -2,7 +2,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
ShortcutEventHandler tabHandler = (editorState, event) {
|
ShortcutEventHandler tabHandler = (editorState, event) {
|
||||||
// Only Supports BulletedList For Now.
|
// Only Supports BulletedList and Checkboxes for now.
|
||||||
|
|
||||||
final selection = editorState.service.selectionService.currentSelection.value;
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
|
|
@ -14,17 +14,14 @@ ShortcutEventHandler tabHandler = (editorState, event) {
|
||||||
final textNode = textNodes.first;
|
final textNode = textNodes.first;
|
||||||
final previous = textNode.previous;
|
final previous = textNode.previous;
|
||||||
|
|
||||||
if (textNode.subtype != BuiltInAttributeKey.bulletedList &&
|
if (textNode.isNotBulletOrCheckbox) {
|
||||||
textNode.subtype != BuiltInAttributeKey.checkbox) {
|
|
||||||
final transaction = editorState.transaction
|
final transaction = editorState.transaction
|
||||||
..insertText(textNode, selection.end.offset, ' ' * 4);
|
..insertText(textNode, selection.end.offset, ' ' * 4);
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previous == null ||
|
if (previous == null || textNode.isNotBulletOrCheckbox) {
|
||||||
(previous.subtype != BuiltInAttributeKey.bulletedList &&
|
|
||||||
previous.subtype != BuiltInAttributeKey.checkbox)) {
|
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||||
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/cursor_left_delete_handler.dart';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/exit_editing_mode_handler.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/exit_editing_mode_handler.dart';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart';
|
||||||
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/outdent_handler.dart';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_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/select_all_handler.dart';
|
||||||
|
|
@ -289,6 +291,11 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
||||||
command: 'tab',
|
command: 'tab',
|
||||||
handler: tabHandler,
|
handler: tabHandler,
|
||||||
),
|
),
|
||||||
|
ShortcutEvent(
|
||||||
|
key: 'Outdent List',
|
||||||
|
command: 'shift+tab',
|
||||||
|
handler: outdentTabHandler,
|
||||||
|
),
|
||||||
ShortcutEvent(
|
ShortcutEvent(
|
||||||
key: 'Backquote to code',
|
key: 'Backquote to code',
|
||||||
command: 'backquote',
|
command: 'backquote',
|
||||||
|
|
@ -312,7 +319,12 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
||||||
ShortcutEvent(
|
ShortcutEvent(
|
||||||
key: 'Underscore to italic',
|
key: 'Underscore to italic',
|
||||||
character: '_',
|
character: '_',
|
||||||
handler: underscoreToItalicHandler,
|
handler: singleUnderscoreToItalicHandler,
|
||||||
|
),
|
||||||
|
ShortcutEvent(
|
||||||
|
key: 'Asterisk to italic',
|
||||||
|
character: '*',
|
||||||
|
handler: singleAsteriskToItalicHandler,
|
||||||
),
|
),
|
||||||
ShortcutEvent(
|
ShortcutEvent(
|
||||||
key: 'Double asterisk to bold',
|
key: 'Double asterisk to bold',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: appflowy_editor
|
name: appflowy_editor
|
||||||
description: A highly customizable rich-text editor for Flutter. The AppFlowy Editor project for AppFlowy and beyond.
|
description: A highly customizable rich-text editor for Flutter. The AppFlowy Editor project for AppFlowy and beyond.
|
||||||
version: 0.1.8
|
version: 0.1.10
|
||||||
homepage: https://github.com/AppFlowy-IO/appflowy-editor
|
homepage: https://github.com/AppFlowy-IO/appflowy-editor
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
|
|
|
||||||
|
|
@ -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/heading_text.dart';
|
||||||
import 'package:appflowy_editor/src/render/rich_text/quoted_text.dart';
|
import 'package:appflowy_editor/src/render/rich_text/quoted_text.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import '../../infra/test_editor.dart';
|
import '../../infra/test_editor.dart';
|
||||||
|
|
@ -31,5 +32,36 @@ void main() async {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(QuotedTextNodeWidget), findsNothing);
|
expect(find.byType(QuotedTextNodeWidget), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('formatHeading', (tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor
|
||||||
|
..insertTextNode(
|
||||||
|
text,
|
||||||
|
attributes: {
|
||||||
|
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
|
||||||
|
BuiltInAttributeKey.heading: BuiltInAttributeKey.h2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await editor.startTesting();
|
||||||
|
await editor.updateSelection(
|
||||||
|
Selection.single(path: [0], startOffset: 0, endOffset: text.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
// format the text to h1
|
||||||
|
formatHeading(editor.editorState, BuiltInAttributeKey.h1);
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
||||||
|
expect(find.byType(HeadingTextNodeWidget), findsOneWidget);
|
||||||
|
|
||||||
|
final textNode = editor.nodeAtPath([0])!;
|
||||||
|
expect(
|
||||||
|
textNode.attributes[BuiltInAttributeKey.subtype],
|
||||||
|
BuiltInAttributeKey.heading,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
textNode.attributes[BuiltInAttributeKey.heading],
|
||||||
|
BuiltInAttributeKey.h1,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue