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
|
||||
* 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">
|
||||
<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>
|
||||
<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>
|
||||
|
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.
|
||||
///
|
||||
/// Please note: The keywords in [BuiltInAttributeKey] are reserved.
|
||||
typedef Attributes = Map<String, dynamic>;
|
||||
|
||||
bool isAttributesEqual(Attributes? a, Attributes? b) {
|
||||
return mapEquals(a, b);
|
||||
}
|
||||
|
||||
Attributes? composeAttributes(
|
||||
Attributes? base,
|
||||
Attributes? other, {
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ export 'text_node_extensions.dart';
|
|||
|
||||
export 'text_style_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;
|
||||
}
|
||||
|
||||
bool get isNotBulletOrCheckbox => ![
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
BuiltInAttributeKey.checkbox
|
||||
].contains(subtype);
|
||||
}
|
||||
|
||||
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/render/style/editor_style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColorOption {
|
||||
|
|
@ -19,6 +21,7 @@ enum _ColorType {
|
|||
class ColorPicker extends StatefulWidget {
|
||||
const ColorPicker({
|
||||
super.key,
|
||||
this.editorState,
|
||||
this.selectedFontColorHex,
|
||||
this.selectedBackgroundColorHex,
|
||||
required this.pickerBackgroundColor,
|
||||
|
|
@ -29,7 +32,7 @@ class ColorPicker extends StatefulWidget {
|
|||
required this.onSubmittedbackgroundColorHex,
|
||||
required this.onSubmittedFontColorHex,
|
||||
});
|
||||
|
||||
final EditorState? editorState;
|
||||
final String? selectedFontColorHex;
|
||||
final String? selectedBackgroundColorHex;
|
||||
final Color pickerBackgroundColor;
|
||||
|
|
@ -53,6 +56,7 @@ class _ColorPickerState extends State<ColorPicker> {
|
|||
TextEditingController();
|
||||
final TextEditingController _backgroundColorOpacityController =
|
||||
TextEditingController();
|
||||
EditorStyle? get style => widget.editorState?.editorStyle;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -131,7 +135,6 @@ class _ColorPickerState extends State<ColorPicker> {
|
|||
return Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
|
|
@ -154,26 +157,15 @@ class _ColorPickerState extends State<ColorPicker> {
|
|||
Widget _buildColorItem(_ColorType type, ColorOption option, bool isChecked) {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
hoverColor: widget.pickerItemHoverColor,
|
||||
onTap: () {
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
if (type == _ColorType.font) {
|
||||
widget.onSubmittedFontColorHex(option.colorHex);
|
||||
} else if (type == _ColorType.background) {
|
||||
widget.onSubmittedbackgroundColorHex(option.colorHex);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// padding
|
||||
const SizedBox(width: 6),
|
||||
// icon
|
||||
SizedBox.square(
|
||||
icon: SizedBox.square(
|
||||
dimension: 12,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -182,19 +174,31 @@ class _ColorPickerState extends State<ColorPicker> {
|
|||
),
|
||||
),
|
||||
),
|
||||
// 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(
|
||||
child: Text(
|
||||
option.name,
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: widget.pickerItemTextColor),
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
// checkbox
|
||||
if (isChecked) const FlowySvg(name: 'checkmark'),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -77,19 +77,19 @@ class _LinkMenuState extends State<LinkMenu> {
|
|||
if (widget.linkText != null) ...[
|
||||
_buildIconButton(
|
||||
iconName: 'link',
|
||||
color: style?.selectionMenuItemIconColor,
|
||||
color: style?.popupMenuFGColor,
|
||||
text: 'Open link',
|
||||
onPressed: widget.onOpenLink,
|
||||
),
|
||||
_buildIconButton(
|
||||
iconName: 'copy',
|
||||
color: style?.selectionMenuItemIconColor,
|
||||
color: style?.popupMenuFGColor,
|
||||
text: 'Copy link',
|
||||
onPressed: widget.onCopyLink,
|
||||
),
|
||||
_buildIconButton(
|
||||
iconName: 'delete',
|
||||
color: style?.selectionMenuItemIconColor,
|
||||
color: style?.popupMenuFGColor,
|
||||
text: 'Remove link',
|
||||
onPressed: widget.onRemoveLink,
|
||||
),
|
||||
|
|
@ -102,11 +102,10 @@ class _LinkMenuState extends State<LinkMenu> {
|
|||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return const Text(
|
||||
return Text(
|
||||
'Add your link',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: style?.textStyle?.fontSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -114,13 +113,17 @@ class _LinkMenuState extends State<LinkMenu> {
|
|||
Widget _buildInput() {
|
||||
return TextField(
|
||||
focusNode: _focusNode,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
style: TextStyle(
|
||||
fontSize: style?.textStyle?.fontSize,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
controller: _textEditingController,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'URL',
|
||||
hintStyle: const TextStyle(fontSize: 14.0),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: style?.textStyle?.fontSize,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16.0),
|
||||
isDense: true,
|
||||
suffixIcon: IconButton(
|
||||
|
|
@ -151,20 +154,24 @@ class _LinkMenuState extends State<LinkMenu> {
|
|||
name: iconName,
|
||||
color: color,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(40),
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
label: Text(
|
||||
text,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
color: style?.selectionMenuItemTextColor ?? Colors.black,
|
||||
color: color,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||
final TextStyle? code;
|
||||
final String? highlightColorHex;
|
||||
|
||||
// Item's pop up menu styles
|
||||
final Color? popupMenuFGColor;
|
||||
final Color? popupMenuHoverColor;
|
||||
|
||||
EditorStyle({
|
||||
required this.padding,
|
||||
required this.backgroundColor,
|
||||
|
|
@ -66,6 +70,8 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||
required this.code,
|
||||
required this.highlightColorHex,
|
||||
required this.lineHeight,
|
||||
required this.popupMenuFGColor,
|
||||
required this.popupMenuHoverColor,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -92,6 +98,8 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||
TextStyle? code,
|
||||
String? highlightColorHex,
|
||||
double? lineHeight,
|
||||
Color? popupMenuFGColor,
|
||||
Color? popupMenuHoverColor,
|
||||
}) {
|
||||
return EditorStyle(
|
||||
padding: padding ?? this.padding,
|
||||
|
|
@ -123,6 +131,8 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||
code: code ?? this.code,
|
||||
highlightColorHex: highlightColorHex ?? this.highlightColorHex,
|
||||
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),
|
||||
highlightColorHex: highlightColorHex,
|
||||
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',
|
||||
lineHeight: 1.5,
|
||||
popupMenuFGColor: const Color(0xFF333333),
|
||||
popupMenuHoverColor: const Color(0xFFE0F8FF),
|
||||
);
|
||||
|
||||
static final dark = light.copyWith(
|
||||
|
|
@ -240,5 +255,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||
selectionMenuItemSelectedColor: const Color(0xFF00BCF0),
|
||||
toolbarColor: const Color(0xFF131720),
|
||||
toolbarElevation: 0.0,
|
||||
popupMenuFGColor: Colors.white,
|
||||
popupMenuHoverColor: const Color(0xFF00BCF0),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,8 +364,7 @@ bool _allSatisfy(
|
|||
);
|
||||
}
|
||||
|
||||
OverlayEntry? _linkMenuOverlay;
|
||||
OverlayEntry? _colorMenuOverlay;
|
||||
OverlayEntry? _overlay;
|
||||
|
||||
EditorState? _editorState;
|
||||
bool _changeSelectionInner = false;
|
||||
|
|
@ -387,7 +386,7 @@ void showLinkMenu(
|
|||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||
matchRect = matchRect.shift(-baseOffset);
|
||||
|
||||
_dismissLinkMenu();
|
||||
_dismissOverlay();
|
||||
_editorState = editorState;
|
||||
|
||||
// Since the link menu will only show in single text selection,
|
||||
|
|
@ -410,7 +409,7 @@ void showLinkMenu(
|
|||
);
|
||||
}
|
||||
|
||||
_linkMenuOverlay = OverlayEntry(
|
||||
_overlay = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned(
|
||||
top: matchRect.bottom + 5.0,
|
||||
|
|
@ -428,11 +427,11 @@ void showLinkMenu(
|
|||
textNode: textNode,
|
||||
);
|
||||
|
||||
_dismissLinkMenu();
|
||||
_dismissOverlay();
|
||||
},
|
||||
onCopyLink: () {
|
||||
AppFlowyClipboard.setData(text: linkText);
|
||||
_dismissLinkMenu();
|
||||
_dismissOverlay();
|
||||
},
|
||||
onRemoveLink: () {
|
||||
final transaction = editorState.transaction
|
||||
|
|
@ -443,7 +442,7 @@ void showLinkMenu(
|
|||
{BuiltInAttributeKey.href: null},
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
_dismissLinkMenu();
|
||||
_dismissOverlay();
|
||||
},
|
||||
onFocusChange: (value) {
|
||||
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.keyboardService?.disable();
|
||||
editorState.service.selectionService.currentSelection
|
||||
.addListener(_dismissLinkMenu);
|
||||
.addListener(_dismissOverlay);
|
||||
}
|
||||
|
||||
void _dismissLinkMenu() {
|
||||
void _dismissOverlay() {
|
||||
// workaround: SelectionService has been released after hot reload.
|
||||
final isSelectionDisposed =
|
||||
_editorState?.service.selectionServiceKey.currentState == null;
|
||||
|
|
@ -479,37 +478,13 @@ void _dismissLinkMenu() {
|
|||
_changeSelectionInner = false;
|
||||
return;
|
||||
}
|
||||
_linkMenuOverlay?.remove();
|
||||
_linkMenuOverlay = null;
|
||||
_overlay?.remove();
|
||||
_overlay = null;
|
||||
|
||||
_editorState?.service.scrollService?.enable();
|
||||
_editorState?.service.keyboardService?.enable();
|
||||
_editorState?.service.selectionService.currentSelection
|
||||
.removeListener(_dismissLinkMenu);
|
||||
_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);
|
||||
.removeListener(_dismissOverlay);
|
||||
_editorState = null;
|
||||
}
|
||||
|
||||
|
|
@ -531,7 +506,7 @@ void showColorMenu(
|
|||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||
matchRect = matchRect.shift(-baseOffset);
|
||||
|
||||
_dismissColorMenu();
|
||||
_dismissOverlay();
|
||||
_editorState = editorState;
|
||||
|
||||
// Since the link menu will only show in single text selection,
|
||||
|
|
@ -563,7 +538,7 @@ void showColorMenu(
|
|||
}
|
||||
|
||||
final style = editorState.editorStyle;
|
||||
_colorMenuOverlay = OverlayEntry(
|
||||
_overlay = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned(
|
||||
top: matchRect.bottom + 5.0,
|
||||
|
|
@ -571,10 +546,11 @@ void showColorMenu(
|
|||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: ColorPicker(
|
||||
editorState: editorState,
|
||||
pickerBackgroundColor:
|
||||
style.selectionMenuBackgroundColor ?? Colors.white,
|
||||
pickerItemHoverColor: style.selectionMenuItemSelectedColor ??
|
||||
Colors.blue.withOpacity(0.3),
|
||||
pickerItemHoverColor:
|
||||
style.popupMenuHoverColor ?? Colors.blue.withOpacity(0.3),
|
||||
pickerItemTextColor:
|
||||
style.selectionMenuItemTextColor ?? Colors.black,
|
||||
selectedFontColorHex: fontColorHex,
|
||||
|
|
@ -587,26 +563,26 @@ void showColorMenu(
|
|||
editorState,
|
||||
color,
|
||||
);
|
||||
_dismissColorMenu();
|
||||
_dismissOverlay();
|
||||
},
|
||||
onSubmittedFontColorHex: (color) {
|
||||
formatFontColor(
|
||||
editorState,
|
||||
color,
|
||||
);
|
||||
_dismissColorMenu();
|
||||
_dismissOverlay();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
Overlay.of(context)?.insert(_colorMenuOverlay!);
|
||||
Overlay.of(context)?.insert(_overlay!);
|
||||
|
||||
editorState.service.scrollService?.disable();
|
||||
editorState.service.keyboardService?.disable();
|
||||
editorState.service.selectionService.currentSelection
|
||||
.addListener(_dismissColorMenu);
|
||||
.addListener(_dismissOverlay);
|
||||
}
|
||||
|
||||
List<ColorOption> _generateFontColorOptions(EditorState editorState) {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,11 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
|
|||
|
||||
for (final textNode in textNodes) {
|
||||
var newAttributes = {...textNode.attributes};
|
||||
if (isAttributesEqual(newAttributes, attributes)) {
|
||||
for (final key in attributes.keys) {
|
||||
newAttributes[key] = null;
|
||||
}
|
||||
} else {
|
||||
for (final globalStyleKey in BuiltInAttributeKey.globalStyleKeys) {
|
||||
if (newAttributes.keys.contains(globalStyleKey)) {
|
||||
newAttributes[globalStyleKey] = null;
|
||||
|
|
@ -115,14 +120,9 @@ bool formatTextNodes(EditorState editorState, Attributes 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]);
|
||||
}
|
||||
newAttributes.addAll(attributes);
|
||||
}
|
||||
|
||||
transaction
|
||||
..updateNode(
|
||||
textNode,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ ShortcutEventHandler cursorLeftSelect = (editorState, event) {
|
|||
if (nodes.isEmpty || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final end = selection.end.goLeft(editorState);
|
||||
final end = selection.end.moveHorizontal(editorState);
|
||||
if (end == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ ShortcutEventHandler cursorRightSelect = (editorState, event) {
|
|||
if (nodes.isEmpty || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final end = selection.end.goRight(editorState);
|
||||
final end = selection.end.moveHorizontal(editorState, moveLeft: false);
|
||||
if (end == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ ShortcutEventHandler cursorUpSelect = (editorState, event) {
|
|||
if (nodes.isEmpty || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final end = _moveVertical(editorState);
|
||||
final end = selection.end.moveVertical(editorState);
|
||||
if (end == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ ShortcutEventHandler cursorDownSelect = (editorState, event) {
|
|||
if (nodes.isEmpty || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final end = _moveVertical(editorState, upwards: false);
|
||||
final end = selection.end.moveVertical(editorState, upwards: false);
|
||||
if (end == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
|
@ -225,7 +225,7 @@ ShortcutEventHandler cursorUp = (editorState, event) {
|
|||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final upPosition = _moveVertical(editorState);
|
||||
final upPosition = selection.end.moveVertical(editorState);
|
||||
editorState.updateCursorSelection(
|
||||
upPosition == null ? null : Selection.collapsed(upPosition),
|
||||
);
|
||||
|
|
@ -241,7 +241,7 @@ ShortcutEventHandler cursorDown = (editorState, event) {
|
|||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final downPosition = _moveVertical(editorState, upwards: false);
|
||||
final downPosition = selection.end.moveVertical(editorState, upwards: false);
|
||||
editorState.updateCursorSelection(
|
||||
downPosition == null ? null : Selection.collapsed(downPosition),
|
||||
);
|
||||
|
|
@ -258,7 +258,7 @@ ShortcutEventHandler cursorLeft = (editorState, event) {
|
|||
}
|
||||
|
||||
Position newPosition = selection.isCollapsed
|
||||
? selection.start.goLeft(editorState) ?? selection.start
|
||||
? selection.start.moveHorizontal(editorState) ?? selection.start
|
||||
: selection.start;
|
||||
|
||||
editorState.service.selectionService.updateSelection(
|
||||
|
|
@ -277,7 +277,8 @@ ShortcutEventHandler cursorRight = (editorState, event) {
|
|||
}
|
||||
|
||||
final newPosition = selection.isCollapsed
|
||||
? selection.start.goRight(editorState) ?? selection.end
|
||||
? selection.start.moveHorizontal(editorState, moveLeft: false) ??
|
||||
selection.end
|
||||
: selection.end;
|
||||
|
||||
editorState.service.selectionService.updateSelection(
|
||||
|
|
@ -294,8 +295,10 @@ ShortcutEventHandler cursorLeftWordSelect = (editorState, event) {
|
|||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final end =
|
||||
selection.end.goLeft(editorState, selectionRange: _SelectionRange.word);
|
||||
final end = selection.end.moveHorizontal(
|
||||
editorState,
|
||||
selectionRange: SelectionRange.word,
|
||||
);
|
||||
if (end == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
|
@ -316,8 +319,10 @@ ShortcutEventHandler cursorLeftWordMove = (editorState, event) {
|
|||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final newPosition = selection.start
|
||||
.goLeft(editorState, selectionRange: _SelectionRange.word) ??
|
||||
final newPosition = selection.start.moveHorizontal(
|
||||
editorState,
|
||||
selectionRange: SelectionRange.word,
|
||||
) ??
|
||||
selection.start;
|
||||
|
||||
editorState.service.selectionService.updateSelection(
|
||||
|
|
@ -336,8 +341,11 @@ ShortcutEventHandler cursorRightWordMove = (editorState, event) {
|
|||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final newPosition = selection.start
|
||||
.goRight(editorState, selectionRange: _SelectionRange.word) ??
|
||||
final newPosition = selection.start.moveHorizontal(
|
||||
editorState,
|
||||
selectionRange: SelectionRange.word,
|
||||
moveLeft: false,
|
||||
) ??
|
||||
selection.end;
|
||||
|
||||
editorState.service.selectionService.updateSelection(
|
||||
|
|
@ -354,8 +362,11 @@ ShortcutEventHandler cursorRightWordSelect = (editorState, event) {
|
|||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final end =
|
||||
selection.end.goRight(editorState, selectionRange: _SelectionRange.word);
|
||||
final end = selection.end.moveHorizontal(
|
||||
editorState,
|
||||
selectionRange: SelectionRange.word,
|
||||
moveLeft: false,
|
||||
);
|
||||
if (end == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
|
@ -367,63 +378,6 @@ ShortcutEventHandler cursorRightWordSelect = (editorState, event) {
|
|||
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 {
|
||||
character,
|
||||
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.
|
||||
if (selection.isCollapsed && selection.start.offset == 0) {
|
||||
if (textNode.toPlainText().isEmpty && textNode.subtype != null) {
|
||||
final path =
|
||||
textNode.path.length > 1 ? [++textNode.path.first] : textNode.path;
|
||||
|
||||
final afterSelection = Selection.collapsed(
|
||||
Position(path: textNode.path, offset: 0),
|
||||
Position(path: path, offset: 0),
|
||||
);
|
||||
final transaction = editorState.transaction
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
if (textNode.path.length > 1) {
|
||||
transaction
|
||||
..deleteNode(textNode)
|
||||
..insertNode(path, textNode)
|
||||
..afterSelection = afterSelection;
|
||||
} else {
|
||||
transaction
|
||||
..updateNode(textNode, {
|
||||
BuiltInAttributeKey.subtype: null,
|
||||
textNode.subtype!: null,
|
||||
})
|
||||
..afterSelection = afterSelection;
|
||||
}
|
||||
editorState.apply(transaction);
|
||||
|
||||
final nextNode = textNode.next;
|
||||
|
|
|
|||
|
|
@ -272,7 +272,17 @@ ShortcutEventHandler markdownLinkOrImageHandler = (editorState, event) {
|
|||
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'
|
||||
// to determine whether the selection is collapsed and whether the selected node is a text node.
|
||||
final selectionService = editorState.service.selectionService;
|
||||
|
|
@ -284,24 +294,24 @@ ShortcutEventHandler underscoreToItalicHandler = (editorState, event) {
|
|||
|
||||
final textNode = textNodes.first;
|
||||
final text = textNode.toPlainText();
|
||||
// Determine if an 'underscore' already exists in the text node and only once.
|
||||
final firstUnderscore = text.indexOf('_');
|
||||
final lastUnderscore = text.lastIndexOf('_');
|
||||
if (firstUnderscore == -1 ||
|
||||
firstUnderscore != lastUnderscore ||
|
||||
firstUnderscore == selection.start.offset - 1) {
|
||||
// Determine if a 'character' already exists in the text node and only once.
|
||||
final firstCharacter = text.indexOf(character);
|
||||
final lastCharacter = text.lastIndexOf(character);
|
||||
if (firstCharacter == -1 ||
|
||||
firstCharacter != lastCharacter ||
|
||||
firstCharacter == selection.start.offset - 1) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
// Delete the previous 'underscore',
|
||||
// update the style of the text surrounded by the two underscores to 'italic',
|
||||
// Delete the previous 'character',
|
||||
// update the style of the text surrounded by the two characters to 'italic',
|
||||
// and update the cursor position.
|
||||
final transaction = editorState.transaction
|
||||
..deleteText(textNode, firstUnderscore, 1)
|
||||
..deleteText(textNode, firstCharacter, 1)
|
||||
..formatText(
|
||||
textNode,
|
||||
firstUnderscore,
|
||||
selection.end.offset - firstUnderscore - 1,
|
||||
firstCharacter,
|
||||
selection.end.offset - firstCharacter - 1,
|
||||
{
|
||||
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';
|
||||
|
||||
ShortcutEventHandler tabHandler = (editorState, event) {
|
||||
// Only Supports BulletedList For Now.
|
||||
// Only Supports BulletedList and Checkboxes for now.
|
||||
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
|
|
@ -14,17 +14,14 @@ ShortcutEventHandler tabHandler = (editorState, event) {
|
|||
final textNode = textNodes.first;
|
||||
final previous = textNode.previous;
|
||||
|
||||
if (textNode.subtype != BuiltInAttributeKey.bulletedList &&
|
||||
textNode.subtype != BuiltInAttributeKey.checkbox) {
|
||||
if (textNode.isNotBulletOrCheckbox) {
|
||||
final transaction = editorState.transaction
|
||||
..insertText(textNode, selection.end.offset, ' ' * 4);
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (previous == null ||
|
||||
(previous.subtype != BuiltInAttributeKey.bulletedList &&
|
||||
previous.subtype != BuiltInAttributeKey.checkbox)) {
|
||||
if (previous == null || textNode.isNotBulletOrCheckbox) {
|
||||
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/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/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/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/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/redo_undo_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',
|
||||
handler: tabHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Outdent List',
|
||||
command: 'shift+tab',
|
||||
handler: outdentTabHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Backquote to code',
|
||||
command: 'backquote',
|
||||
|
|
@ -312,7 +319,12 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
|||
ShortcutEvent(
|
||||
key: 'Underscore to italic',
|
||||
character: '_',
|
||||
handler: underscoreToItalicHandler,
|
||||
handler: singleUnderscoreToItalicHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Asterisk to italic',
|
||||
character: '*',
|
||||
handler: singleAsteriskToItalicHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Double asterisk to bold',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name: appflowy_editor
|
||||
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
|
||||
|
||||
platforms:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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:flutter_test/flutter_test.dart';
|
||||
import '../../infra/test_editor.dart';
|
||||
|
|
@ -31,5 +32,36 @@ void main() async {
|
|||
await tester.pumpAndSettle();
|
||||
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