feat: implement theme customizer

This commit is contained in:
Lucas.Xu 2022-09-15 21:27:49 +08:00
parent 988e8db798
commit 68997a9c93
81 changed files with 2977 additions and 860 deletions

View File

@ -22,8 +22,8 @@ editor.insertTextNode(text);
// Insert the same text, but with the heading style.
editor.insertTextNode(text, attributes: {
StyleKey.subtype: StyleKey.heading,
StyleKey.heading: StyleKey.h1,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
});
// Insert our text with the bulleted list style and the bold style.
@ -31,10 +31,10 @@ editor.insertTextNode(text, attributes: {
editor.insertTextNode(
'',
attributes: {
StyleKey.subtype: StyleKey.bulletedList,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
},
delta: Delta([
TextInsert(text, {StyleKey.bold: true}),
TextInsert(text, {BuiltInAttributeKey.bold: true}),
]),
);
```

View File

@ -21,6 +21,10 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: const [
AppFlowyEditorLocalizations.delegate,
],
supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales,
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
@ -40,7 +44,9 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> {
int _pageIndex = 0;
late EditorState _editorState;
EditorState? _editorState;
bool darkMode = false;
EditorStyle _editorStyle = EditorStyle.defaultStyle();
Future<String>? _jsonString;
@override
@ -78,24 +84,29 @@ class _MyHomePageState extends State<MyHomePage> {
return FutureBuilder<String>(
future: jsonString,
builder: (_, snapshot) {
if (snapshot.hasData) {
_editorState = EditorState(
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
_editorState ??= EditorState(
document: StateTree.fromJson(
Map<String, Object>.from(
json.decode(snapshot.data!),
),
),
);
_editorState.logConfiguration
_editorState!.logConfiguration
..level = LogLevel.all
..handler = (message) {
debugPrint(message);
};
return SizedBox(
_editorState!.operationStream.listen((event) {
debugPrint('Operation: ${event.toJson()}');
});
return Container(
color: darkMode ? Colors.black : Colors.white,
width: MediaQuery.of(context).size.width,
child: AppFlowyEditor(
editorState: _editorState,
editorStyle: const EditorStyle.defaultStyle(),
editorState: _editorState!,
editorStyle: _editorStyle,
shortcutEvents: [
underscoreToItalicEvent,
],
@ -127,12 +138,26 @@ class _MyHomePageState extends State<MyHomePage> {
onPressed: () => _switchToPage(2),
),
ActionButton(
icon: const Icon(Icons.print),
onPressed: () => {_exportDocument(_editorState)}),
icon: const Icon(Icons.print),
onPressed: () => _exportDocument(_editorState!),
),
ActionButton(
icon: const Icon(Icons.import_export),
onPressed: () => _importDocument(),
),
ActionButton(
icon: const Icon(Icons.color_lens),
onPressed: () {
setState(() {
_editorStyle = _editorStyle.copyWith(
textStyle: darkMode
? BuiltInTextStyle.builtIn()
: BuiltInTextStyle.builtInDarkMode(),
);
darkMode = !darkMode;
});
},
),
],
);
}
@ -166,6 +191,7 @@ class _MyHomePageState extends State<MyHomePage> {
void _switchToPage(int pageIndex) {
if (pageIndex != _pageIndex) {
setState(() {
_editorState = null;
_pageIndex = pageIndex;
});
}

View File

@ -26,3 +26,5 @@ export 'src/service/input_service.dart';
export 'src/service/shortcut_event/keybinding.dart';
export 'src/service/shortcut_event/shortcut_event.dart';
export 'src/service/shortcut_event/shortcut_event_handler.dart';
export 'src/extensions/attributes_extension.dart';
export 'src/l10n/l10n.dart';

View File

@ -0,0 +1,35 @@
{
"@@locale": "ca",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "de-DE",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "en",
"bold": "Bold",
"@bold": {},
"bulletedList": "Bulleted List",
"@bulletedList": {},
"checkbox": "Checkbox",
"@checkbox": {},
"embedCode": "Embed Code",
"@embedCode": {},
"heading1": "H1",
"@heading1": {},
"heading2": "H2",
"@heading2": {},
"heading3": "H3",
"@heading3": {},
"highlight": "Highlight",
"@highlight": {},
"image": "Image",
"@image": {},
"italic": "Italic",
"@italic": {},
"link": "Link",
"@link": {},
"numberedList": "Numbered List",
"@numberedList": {},
"quote": "Quote",
"@quote": {},
"strikethrough": "Strikethrough",
"@strikethrough": {},
"text": "Text",
"@text": {},
"underline": "Underline",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "es-VE",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "fr-CA",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "fr-FR",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "hu-HU",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "id-ID",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "it-IT",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "ja-JP",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "pl-PL",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "pt-BR",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "pt-PT",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "ru-RU",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "tr-TR",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "zh-CN",
"bold": "加粗",
"@bold": {},
"bulletedList": "无序列表",
"@bulletedList": {},
"checkbox": "复选框",
"@checkbox": {},
"embedCode": "内嵌代码",
"@embedCode": {},
"heading1": "标题 1",
"@heading1": {},
"heading2": "标题 2",
"@heading2": {},
"heading3": "标题 3",
"@heading3": {},
"highlight": "高亮",
"@highlight": {},
"image": "图片",
"@image": {},
"italic": "斜体",
"@italic": {},
"link": "链接",
"@link": {},
"numberedList": "有序列表",
"@numberedList": {},
"quote": "引用",
"@quote": {},
"strikethrough": "删除线",
"@strikethrough": {},
"text": "文字",
"@text": {},
"underline": "下划线",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "zh-TW",
"bold": "",
"@bold": {},
"bulletedList": "",
"@bulletedList": {},
"checkbox": "",
"@checkbox": {},
"embedCode": "",
"@embedCode": {},
"heading1": "",
"@heading1": {},
"heading2": "",
"@heading2": {},
"heading3": "",
"@heading3": {},
"highlight": "",
"@highlight": {},
"image": "",
"@image": {},
"italic": "",
"@italic": {},
"link": "",
"@link": {},
"numberedList": "",
"@numberedList": {},
"quote": "",
"@quote": {},
"strikethrough": "",
"@strikethrough": {},
"text": "",
"@text": {},
"underline": "",
"@underline": {}
}

View File

@ -0,0 +1,59 @@
///
/// Supported partial rendering types:
/// bold, italic,
/// underline, strikethrough,
/// color, font,
/// href
///
/// Supported global rendering types:
/// heading: h1, h2, h3, h4, h5, h6, ...
/// block quote,
/// list: ordered list, bulleted list,
/// code block
///
class BuiltInAttributeKey {
static String bold = 'bold';
static String italic = 'italic';
static String underline = 'underline';
static String strikethrough = 'strikethrough';
static String color = 'color';
static String backgroundColor = 'backgroundColor';
static String font = 'font';
static String href = 'href';
static String subtype = 'subtype';
static String heading = 'heading';
static String h1 = 'h1';
static String h2 = 'h2';
static String h3 = 'h3';
static String h4 = 'h4';
static String h5 = 'h5';
static String h6 = 'h6';
static String bulletedList = 'bulleted-list';
static String numberList = 'number-list';
static String quote = 'quote';
static String checkbox = 'checkbox';
static String code = 'code';
static String number = 'number';
static List<String> partialStyleKeys = [
BuiltInAttributeKey.bold,
BuiltInAttributeKey.italic,
BuiltInAttributeKey.underline,
BuiltInAttributeKey.strikethrough,
BuiltInAttributeKey.backgroundColor,
BuiltInAttributeKey.href,
BuiltInAttributeKey.code,
];
static List<String> globalStyleKeys = [
BuiltInAttributeKey.subtype,
BuiltInAttributeKey.heading,
BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.bulletedList,
BuiltInAttributeKey.numberList,
BuiltInAttributeKey.quote,
];
}

View File

@ -24,6 +24,13 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
return null;
}
String get id {
if (subtype != null) {
return '$type/$subtype';
}
return type;
}
Path get path => _path();
Attributes get attributes => _attributes;

View File

@ -60,7 +60,11 @@ class EditorState {
List<SelectionMenuItem> selectionMenuItems = [];
/// Stores the editor style.
EditorStyle editorStyle = const EditorStyle.defaultStyle();
EditorStyle editorStyle = EditorStyle.defaultStyle();
/// Operation stream.
Stream<Operation> get operationStream => _observer.stream;
final StreamController<Operation> _observer = StreamController.broadcast();
final UndoManager undoManager = UndoManager();
Selection? _cursorSelection;
@ -72,6 +76,15 @@ class EditorState {
return _cursorSelection;
}
RenderBox? get renderBox {
final renderObject =
service.scrollServiceKey.currentContext?.findRenderObject();
if (renderObject != null && renderObject is RenderBox) {
return renderObject;
}
return null;
}
updateCursorSelection(Selection? cursorSelection,
[CursorUpdateReason reason = CursorUpdateReason.others]) {
// broadcast to other users here
@ -151,5 +164,6 @@ class EditorState {
} else if (op is TextEditOperation) {
document.textEdit(op.path, op.delta);
}
_observer.add(op);
}
}

View File

@ -0,0 +1,93 @@
import 'package:appflowy_editor/src/document/attributes.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:flutter/material.dart';
extension NodeAttributesExtensions on Attributes {
String? get heading {
if (containsKey(BuiltInAttributeKey.subtype) &&
containsKey(BuiltInAttributeKey.heading) &&
this[BuiltInAttributeKey.subtype] == BuiltInAttributeKey.heading &&
this[BuiltInAttributeKey.heading] is String) {
return this[BuiltInAttributeKey.heading];
}
return null;
}
bool get quote {
return containsKey(BuiltInAttributeKey.quote);
}
int? get number {
if (containsKey(BuiltInAttributeKey.number) &&
this[BuiltInAttributeKey.number] is int) {
return this[BuiltInAttributeKey.number];
}
return null;
}
bool get code {
if (containsKey(BuiltInAttributeKey.code) &&
this[BuiltInAttributeKey.code] == true) {
return this[BuiltInAttributeKey.code];
}
return false;
}
bool get check {
if (containsKey(BuiltInAttributeKey.checkbox) &&
this[BuiltInAttributeKey.checkbox] is bool) {
return this[BuiltInAttributeKey.checkbox];
}
return false;
}
}
extension DeltaAttributesExtensions on Attributes {
bool get bold {
return (containsKey(BuiltInAttributeKey.bold) &&
this[BuiltInAttributeKey.bold] == true);
}
bool get italic {
return (containsKey(BuiltInAttributeKey.italic) &&
this[BuiltInAttributeKey.italic] == true);
}
bool get underline {
return (containsKey(BuiltInAttributeKey.underline) &&
this[BuiltInAttributeKey.underline] == true);
}
bool get strikethrough {
return (containsKey(BuiltInAttributeKey.strikethrough) &&
this[BuiltInAttributeKey.strikethrough] == true);
}
Color? get color {
if (containsKey(BuiltInAttributeKey.color) &&
this[BuiltInAttributeKey.color] is String) {
return Color(
int.parse(this[BuiltInAttributeKey.color]),
);
}
return null;
}
Color? get backgroundColor {
if (containsKey(BuiltInAttributeKey.backgroundColor) &&
this[BuiltInAttributeKey.backgroundColor] is String) {
return Color(
int.parse(this[BuiltInAttributeKey.backgroundColor]),
);
}
return null;
}
String? get href {
if (containsKey(BuiltInAttributeKey.href) &&
this[BuiltInAttributeKey.href] is String) {
return this[BuiltInAttributeKey.href];
}
return null;
}
}

View File

@ -3,7 +3,7 @@ import 'package:appflowy_editor/src/document/path.dart';
import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart';
import 'package:appflowy_editor/src/document/text_delta.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
extension TextNodeExtension on TextNode {
dynamic getAttributeInSelection(Selection selection, String styleKey) {
@ -29,27 +29,28 @@ extension TextNodeExtension on TextNode {
}
bool allSatisfyLinkInSelection(Selection selection) =>
allSatisfyInSelection(selection, StyleKey.href, (value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.href, (value) {
return value != null;
});
bool allSatisfyBoldInSelection(Selection selection) =>
allSatisfyInSelection(selection, StyleKey.bold, (value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) {
return value == true;
});
bool allSatisfyItalicInSelection(Selection selection) =>
allSatisfyInSelection(selection, StyleKey.italic, (value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.italic, (value) {
return value == true;
});
bool allSatisfyUnderlineInSelection(Selection selection) =>
allSatisfyInSelection(selection, StyleKey.underline, (value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.underline, (value) {
return value == true;
});
bool allSatisfyStrikethroughInSelection(Selection selection) =>
allSatisfyInSelection(selection, StyleKey.strikethrough, (value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.strikethrough,
(value) {
return value == true;
});
@ -58,11 +59,11 @@ extension TextNodeExtension on TextNode {
String styleKey,
bool Function(dynamic value) test,
) {
if (StyleKey.globalStyleKeys.contains(styleKey)) {
if (BuiltInAttributeKey.globalStyleKeys.contains(styleKey)) {
if (attributes.containsKey(styleKey)) {
return test(attributes[styleKey]);
}
} else if (StyleKey.partialStyleKeys.contains(styleKey)) {
} else if (BuiltInAttributeKey.partialStyleKeys.contains(styleKey)) {
final ops = delta.whereType<TextInsert>();
final startOffset =
selection.isBackward ? selection.start.offset : selection.end.offset;
@ -120,28 +121,28 @@ extension TextNodeExtension on TextNode {
extension TextNodesExtension on List<TextNode> {
bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection(
selection,
StyleKey.bold,
BuiltInAttributeKey.bold,
(value) => value == true,
);
bool allSatisfyItalicInSelection(Selection selection) =>
allSatisfyInSelection(
selection,
StyleKey.italic,
BuiltInAttributeKey.italic,
(value) => value == true,
);
bool allSatisfyUnderlineInSelection(Selection selection) =>
allSatisfyInSelection(
selection,
StyleKey.underline,
BuiltInAttributeKey.underline,
(value) => value == true,
);
bool allSatisfyStrikethroughInSelection(Selection selection) =>
allSatisfyInSelection(
selection,
StyleKey.strikethrough,
BuiltInAttributeKey.strikethrough,
(value) => value == true,
);

View File

@ -0,0 +1,73 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
extension TextSpanExtensions on TextSpan {
TextSpan copyWith({
String? text,
TextStyle? style,
List<InlineSpan>? children,
GestureRecognizer? recognizer,
String? semanticsLabel,
}) {
return TextSpan(
text: text ?? this.text,
style: style ?? this.style,
children: children ?? this.children,
recognizer: recognizer ?? this.recognizer,
semanticsLabel: semanticsLabel ?? this.semanticsLabel,
);
}
TextSpan updateTextStyle(TextStyle? other) {
if (other == null) {
return this;
}
return copyWith(
style: style?.combine(other),
children: children?.map((child) {
if (child is TextSpan) {
return child.updateTextStyle(other);
}
return child;
}).toList(growable: false),
);
}
}
extension TextStyleExtensions on TextStyle {
TextStyle combine(TextStyle? other) {
if (other == null) {
return this;
}
if (!other.inherit) {
return other;
}
return copyWith(
color: other.color,
backgroundColor: other.backgroundColor,
fontSize: other.fontSize,
fontWeight: other.fontWeight,
fontStyle: other.fontStyle,
letterSpacing: other.letterSpacing,
wordSpacing: other.wordSpacing,
textBaseline: other.textBaseline,
height: other.height,
leadingDistribution: other.leadingDistribution,
locale: other.locale,
foreground: other.foreground,
background: other.background,
shadows: other.shadows,
fontFeatures: other.fontFeatures,
decoration: TextDecoration.combine([
if (decoration != null) decoration!,
if (other.decoration != null) other.decoration!,
]),
decorationColor: other.decorationColor,
decorationStyle: other.decorationStyle,
decorationThickness: other.decorationThickness,
fontFamilyFallback: other.fontFamilyFallback,
overflow: other.overflow,
);
}
}

View File

@ -4,11 +4,11 @@ import 'dart:ui';
import 'package:appflowy_editor/src/document/attributes.dart';
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/document/text_delta.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/extensions/color_extension.dart';
import 'package:flutter/material.dart';
import 'package:html/parser.dart' show parse;
import 'package:html/dom.dart' as html;
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
class HTMLTag {
static const h1 = "h1";
@ -99,7 +99,8 @@ class HTMLToNodesConverter {
for (final child in element.nodes.toList()) {
if (child is html.Element) {
result.addAll(_handleElement(child, {"subtype": StyleKey.quote}));
result.addAll(
_handleElement(child, {"subtype": BuiltInAttributeKey.quote}));
}
}
@ -174,11 +175,11 @@ class HTMLToNodesConverter {
final fontWeightStr = cssMap["font-weight"];
if (fontWeightStr != null) {
if (fontWeightStr == "bold") {
attrs[StyleKey.bold] = true;
attrs[BuiltInAttributeKey.bold] = true;
} else {
int? weight = int.tryParse(fontWeightStr);
if (weight != null && weight > 500) {
attrs[StyleKey.bold] = true;
attrs[BuiltInAttributeKey.bold] = true;
}
}
}
@ -193,12 +194,12 @@ class HTMLToNodesConverter {
? null
: ColorExtension.tryFromRgbaString(backgroundColorStr);
if (backgroundColor != null) {
attrs[StyleKey.backgroundColor] =
attrs[BuiltInAttributeKey.backgroundColor] =
'0x${backgroundColor.value.toRadixString(16)}';
}
if (cssMap["font-style"] == "italic") {
attrs[StyleKey.italic] = true;
attrs[BuiltInAttributeKey.italic] = true;
}
return attrs.isEmpty ? null : attrs;
@ -208,9 +209,9 @@ class HTMLToNodesConverter {
final decorations = decorationStr.split(" ");
for (final d in decorations) {
if (d == "line-through") {
attrs[StyleKey.strikethrough] = true;
attrs[BuiltInAttributeKey.strikethrough] = true;
} else if (d == "underline") {
attrs[StyleKey.underline] = true;
attrs[BuiltInAttributeKey.underline] = true;
}
}
}
@ -228,13 +229,13 @@ class HTMLToNodesConverter {
delta.insert(element.text, attributes);
} else if (element.localName == HTMLTag.strong ||
element.localName == HTMLTag.bold) {
delta.insert(element.text, {StyleKey.bold: true});
delta.insert(element.text, {BuiltInAttributeKey.bold: true});
} else if (element.localName == HTMLTag.underline) {
delta.insert(element.text, {StyleKey.underline: true});
delta.insert(element.text, {BuiltInAttributeKey.underline: true});
} else if (element.localName == HTMLTag.italic) {
delta.insert(element.text, {StyleKey.italic: true});
delta.insert(element.text, {BuiltInAttributeKey.italic: true});
} else if (element.localName == HTMLTag.del) {
delta.insert(element.text, {StyleKey.strikethrough: true});
delta.insert(element.text, {BuiltInAttributeKey.strikethrough: true});
} else {
delta.insert(element.text);
}
@ -273,7 +274,7 @@ class HTMLToNodesConverter {
final textNode =
TextNode(type: "text", delta: delta, attributes: attributes);
if (isCheckbox) {
textNode.attributes["subtype"] = StyleKey.checkbox;
textNode.attributes["subtype"] = BuiltInAttributeKey.checkbox;
textNode.attributes["checkbox"] = checked;
}
return textNode;
@ -291,8 +292,8 @@ class HTMLToNodesConverter {
List<Node> _handleUnorderedList(html.Element element) {
final result = <Node>[];
for (var child in element.children) {
result.addAll(
_handleListElement(child, {"subtype": StyleKey.bulletedList}));
result.addAll(_handleListElement(
child, {"subtype": BuiltInAttributeKey.bulletedList}));
}
return result;
}
@ -302,7 +303,7 @@ class HTMLToNodesConverter {
for (var i = 0; i < element.children.length; i++) {
final child = element.children[i];
result.addAll(_handleListElement(
child, {"subtype": StyleKey.numberList, "number": i + 1}));
child, {"subtype": BuiltInAttributeKey.numberList, "number": i + 1}));
}
return result;
}
@ -401,7 +402,8 @@ class NodesToHTMLConverter {
_addElement(TextNode textNode, html.Element element) {
if (element.localName == HTMLTag.list) {
final isNumbered = textNode.attributes["subtype"] == StyleKey.numberList;
final isNumbered =
textNode.attributes["subtype"] == BuiltInAttributeKey.numberList;
_stashListContainer ??= html.Element.tag(
isNumbered ? HTMLTag.orderedList : HTMLTag.unorderedList);
_stashListContainer?.append(element);
@ -433,10 +435,10 @@ class NodesToHTMLConverter {
String _textDecorationsFromAttributes(Attributes attributes) {
var textDecoration = <String>[];
if (attributes[StyleKey.strikethrough] == true) {
if (attributes[BuiltInAttributeKey.strikethrough] == true) {
textDecoration.add("line-through");
}
if (attributes[StyleKey.underline] == true) {
if (attributes[BuiltInAttributeKey.underline] == true) {
textDecoration.add("underline");
}
@ -445,19 +447,19 @@ class NodesToHTMLConverter {
String _attributesToCssStyle(Map<String, dynamic> attributes) {
final cssMap = <String, String>{};
if (attributes[StyleKey.backgroundColor] != null) {
if (attributes[BuiltInAttributeKey.backgroundColor] != null) {
final color = Color(
int.parse(attributes[StyleKey.backgroundColor]),
int.parse(attributes[BuiltInAttributeKey.backgroundColor]),
);
cssMap["background-color"] = color.toRgbaString();
}
if (attributes[StyleKey.color] != null) {
if (attributes[BuiltInAttributeKey.color] != null) {
final color = Color(
int.parse(attributes[StyleKey.color]),
int.parse(attributes[BuiltInAttributeKey.color]),
);
cssMap["color"] = color.toRgbaString();
}
if (attributes[StyleKey.bold] == true) {
if (attributes[BuiltInAttributeKey.bold] == true) {
cssMap["font-weight"] = "bold";
}
@ -466,7 +468,7 @@ class NodesToHTMLConverter {
cssMap["text-decoration"] = textDecoration;
}
if (attributes[StyleKey.italic] == true) {
if (attributes[BuiltInAttributeKey.italic] == true) {
cssMap["font-style"] = "italic";
}
return _cssMapToCssStyle(cssMap);
@ -507,23 +509,24 @@ class NodesToHTMLConverter {
final childNodes = <html.Node>[];
String tagName = HTMLTag.paragraph;
if (subType == StyleKey.bulletedList || subType == StyleKey.numberList) {
if (subType == BuiltInAttributeKey.bulletedList ||
subType == BuiltInAttributeKey.numberList) {
tagName = HTMLTag.list;
} else if (subType == StyleKey.checkbox) {
} else if (subType == BuiltInAttributeKey.checkbox) {
final node = html.Element.html('<input type="checkbox" />');
if (checked != null && checked) {
node.attributes["checked"] = "true";
}
childNodes.add(node);
} else if (subType == StyleKey.heading) {
if (heading == StyleKey.h1) {
} else if (subType == BuiltInAttributeKey.heading) {
if (heading == BuiltInAttributeKey.h1) {
tagName = HTMLTag.h1;
} else if (heading == StyleKey.h2) {
} else if (heading == BuiltInAttributeKey.h2) {
tagName = HTMLTag.h2;
} else if (heading == StyleKey.h3) {
} else if (heading == BuiltInAttributeKey.h3) {
tagName = HTMLTag.h3;
}
} else if (subType == StyleKey.quote) {
} else if (subType == BuiltInAttributeKey.quote) {
tagName = HTMLTag.blockQuote;
}
@ -531,22 +534,23 @@ class NodesToHTMLConverter {
if (op is TextInsert) {
final attributes = op.attributes;
if (attributes != null) {
if (attributes.length == 1 && attributes[StyleKey.bold] == true) {
if (attributes.length == 1 &&
attributes[BuiltInAttributeKey.bold] == true) {
final strong = html.Element.tag(HTMLTag.strong);
strong.append(html.Text(op.content));
childNodes.add(strong);
} else if (attributes.length == 1 &&
attributes[StyleKey.underline] == true) {
attributes[BuiltInAttributeKey.underline] == true) {
final strong = html.Element.tag(HTMLTag.underline);
strong.append(html.Text(op.content));
childNodes.add(strong);
} else if (attributes.length == 1 &&
attributes[StyleKey.italic] == true) {
attributes[BuiltInAttributeKey.italic] == true) {
final strong = html.Element.tag(HTMLTag.italic);
strong.append(html.Text(op.content));
childNodes.add(strong);
} else if (attributes.length == 1 &&
attributes[StyleKey.strikethrough] == true) {
attributes[BuiltInAttributeKey.strikethrough] == true) {
final strong = html.Element.tag(HTMLTag.del);
strong.append(html.Text(op.content));
childNodes.add(strong);

View File

@ -0,0 +1,126 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that looks up messages for specific locales by
// delegating to the appropriate library.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:implementation_imports, file_names, unnecessary_new
// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
// ignore_for_file:argument_type_not_assignable, invalid_assignment
// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
// ignore_for_file:comment_references
import 'dart:async';
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_ca.dart' as messages_ca;
import 'messages_de-DE.dart' as messages_de_de;
import 'messages_en.dart' as messages_en;
import 'messages_es-VE.dart' as messages_es_ve;
import 'messages_fr-CA.dart' as messages_fr_ca;
import 'messages_fr-FR.dart' as messages_fr_fr;
import 'messages_hu-HU.dart' as messages_hu_hu;
import 'messages_id-ID.dart' as messages_id_id;
import 'messages_it-IT.dart' as messages_it_it;
import 'messages_ja-JP.dart' as messages_ja_jp;
import 'messages_pl-PL.dart' as messages_pl_pl;
import 'messages_pt-BR.dart' as messages_pt_br;
import 'messages_pt-PT.dart' as messages_pt_pt;
import 'messages_ru-RU.dart' as messages_ru_ru;
import 'messages_tr-TR.dart' as messages_tr_tr;
import 'messages_zh-CN.dart' as messages_zh_cn;
import 'messages_zh-TW.dart' as messages_zh_tw;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'ca': () => new Future.value(null),
'de_DE': () => new Future.value(null),
'en': () => new Future.value(null),
'es_VE': () => new Future.value(null),
'fr_CA': () => new Future.value(null),
'fr_FR': () => new Future.value(null),
'hu_HU': () => new Future.value(null),
'id_ID': () => new Future.value(null),
'it_IT': () => new Future.value(null),
'ja_JP': () => new Future.value(null),
'pl_PL': () => new Future.value(null),
'pt_BR': () => new Future.value(null),
'pt_PT': () => new Future.value(null),
'ru_RU': () => new Future.value(null),
'tr_TR': () => new Future.value(null),
'zh_CN': () => new Future.value(null),
'zh_TW': () => new Future.value(null),
};
MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'ca':
return messages_ca.messages;
case 'de_DE':
return messages_de_de.messages;
case 'en':
return messages_en.messages;
case 'es_VE':
return messages_es_ve.messages;
case 'fr_CA':
return messages_fr_ca.messages;
case 'fr_FR':
return messages_fr_fr.messages;
case 'hu_HU':
return messages_hu_hu.messages;
case 'id_ID':
return messages_id_id.messages;
case 'it_IT':
return messages_it_it.messages;
case 'ja_JP':
return messages_ja_jp.messages;
case 'pl_PL':
return messages_pl_pl.messages;
case 'pt_BR':
return messages_pt_br.messages;
case 'pt_PT':
return messages_pt_pt.messages;
case 'ru_RU':
return messages_ru_ru.messages;
case 'tr_TR':
return messages_tr_tr.messages;
case 'zh_CN':
return messages_zh_cn.messages;
case 'zh_TW':
return messages_zh_tw.messages;
default:
return null;
}
}
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) async {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new Future.value(false);
}
var lib = _deferredLibraries[availableLocale];
await (lib == null ? new Future.value(false) : lib());
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new Future.value(true);
}
bool _messagesExistFor(String locale) {
try {
return _findExact(locale) != null;
} catch (e) {
return false;
}
}
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale =
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
if (actualLocale == null) return null;
return _findExact(actualLocale);
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ca locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'ca';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a de_DE locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'de_DE';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a en locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage("Bold"),
"bulletedList": MessageLookupByLibrary.simpleMessage("Bulleted List"),
"checkbox": MessageLookupByLibrary.simpleMessage("Checkbox"),
"embedCode": MessageLookupByLibrary.simpleMessage("Embed Code"),
"heading1": MessageLookupByLibrary.simpleMessage("H1"),
"heading2": MessageLookupByLibrary.simpleMessage("H2"),
"heading3": MessageLookupByLibrary.simpleMessage("H3"),
"highlight": MessageLookupByLibrary.simpleMessage("Highlight"),
"image": MessageLookupByLibrary.simpleMessage("Image"),
"italic": MessageLookupByLibrary.simpleMessage("Italic"),
"link": MessageLookupByLibrary.simpleMessage("Link"),
"numberedList": MessageLookupByLibrary.simpleMessage("Numbered List"),
"quote": MessageLookupByLibrary.simpleMessage("Quote"),
"strikethrough": MessageLookupByLibrary.simpleMessage("Strikethrough"),
"text": MessageLookupByLibrary.simpleMessage("Text"),
"underline": MessageLookupByLibrary.simpleMessage("Underline")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a es_VE locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'es_VE';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a fr_CA locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'fr_CA';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a fr_FR locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'fr_FR';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a hu_HU locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'hu_HU';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a id_ID locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'id_ID';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a it_IT locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'it_IT';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ja_JP locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'ja_JP';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a pl_PL locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'pl_PL';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a pt_BR locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'pt_BR';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a pt_PT locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'pt_PT';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ru_RU locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'ru_RU';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a tr_TR locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'tr_TR';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a zh_CN locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'zh_CN';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage("加粗"),
"bulletedList": MessageLookupByLibrary.simpleMessage("无序列表"),
"checkbox": MessageLookupByLibrary.simpleMessage("复选框"),
"embedCode": MessageLookupByLibrary.simpleMessage("内嵌代码"),
"heading1": MessageLookupByLibrary.simpleMessage("标题 1"),
"heading2": MessageLookupByLibrary.simpleMessage("标题 2"),
"heading3": MessageLookupByLibrary.simpleMessage("标题 3"),
"highlight": MessageLookupByLibrary.simpleMessage("高亮"),
"image": MessageLookupByLibrary.simpleMessage("图片"),
"italic": MessageLookupByLibrary.simpleMessage("斜体"),
"link": MessageLookupByLibrary.simpleMessage("链接"),
"numberedList": MessageLookupByLibrary.simpleMessage("有序列表"),
"quote": MessageLookupByLibrary.simpleMessage("引用"),
"strikethrough": MessageLookupByLibrary.simpleMessage("删除线"),
"text": MessageLookupByLibrary.simpleMessage("文字"),
"underline": MessageLookupByLibrary.simpleMessage("下划线")
};
}

View File

@ -0,0 +1,42 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a zh_TW locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'zh_TW';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""),
"bulletedList": MessageLookupByLibrary.simpleMessage(""),
"checkbox": MessageLookupByLibrary.simpleMessage(""),
"embedCode": MessageLookupByLibrary.simpleMessage(""),
"heading1": MessageLookupByLibrary.simpleMessage(""),
"heading2": MessageLookupByLibrary.simpleMessage(""),
"heading3": MessageLookupByLibrary.simpleMessage(""),
"highlight": MessageLookupByLibrary.simpleMessage(""),
"image": MessageLookupByLibrary.simpleMessage(""),
"italic": MessageLookupByLibrary.simpleMessage(""),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage(""),
"quote": MessageLookupByLibrary.simpleMessage(""),
"strikethrough": MessageLookupByLibrary.simpleMessage(""),
"text": MessageLookupByLibrary.simpleMessage(""),
"underline": MessageLookupByLibrary.simpleMessage("")
};
}

View File

@ -0,0 +1,257 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'intl/messages_all.dart';
// **************************************************************************
// Generator: Flutter Intl IDE plugin
// Made by Localizely
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each
// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes
class AppFlowyEditorLocalizations {
AppFlowyEditorLocalizations();
static AppFlowyEditorLocalizations? _current;
static AppFlowyEditorLocalizations get current {
assert(_current != null,
'No instance of AppFlowyEditorLocalizations was loaded. Try to initialize the AppFlowyEditorLocalizations delegate before accessing AppFlowyEditorLocalizations.current.');
return _current!;
}
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<AppFlowyEditorLocalizations> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
final instance = AppFlowyEditorLocalizations();
AppFlowyEditorLocalizations._current = instance;
return instance;
});
}
static AppFlowyEditorLocalizations of(BuildContext context) {
final instance = AppFlowyEditorLocalizations.maybeOf(context);
assert(instance != null,
'No instance of AppFlowyEditorLocalizations present in the widget tree. Did you add AppFlowyEditorLocalizations.delegate in localizationsDelegates?');
return instance!;
}
static AppFlowyEditorLocalizations? maybeOf(BuildContext context) {
return Localizations.of<AppFlowyEditorLocalizations>(
context, AppFlowyEditorLocalizations);
}
/// `Bold`
String get bold {
return Intl.message(
'Bold',
name: 'bold',
desc: '',
args: [],
);
}
/// `Bulleted List`
String get bulletedList {
return Intl.message(
'Bulleted List',
name: 'bulletedList',
desc: '',
args: [],
);
}
/// `Checkbox`
String get checkbox {
return Intl.message(
'Checkbox',
name: 'checkbox',
desc: '',
args: [],
);
}
/// `Embed Code`
String get embedCode {
return Intl.message(
'Embed Code',
name: 'embedCode',
desc: '',
args: [],
);
}
/// `H1`
String get heading1 {
return Intl.message(
'H1',
name: 'heading1',
desc: '',
args: [],
);
}
/// `H2`
String get heading2 {
return Intl.message(
'H2',
name: 'heading2',
desc: '',
args: [],
);
}
/// `H3`
String get heading3 {
return Intl.message(
'H3',
name: 'heading3',
desc: '',
args: [],
);
}
/// `Highlight`
String get highlight {
return Intl.message(
'Highlight',
name: 'highlight',
desc: '',
args: [],
);
}
/// `Image`
String get image {
return Intl.message(
'Image',
name: 'image',
desc: '',
args: [],
);
}
/// `Italic`
String get italic {
return Intl.message(
'Italic',
name: 'italic',
desc: '',
args: [],
);
}
/// `Link`
String get link {
return Intl.message(
'Link',
name: 'link',
desc: '',
args: [],
);
}
/// `Numbered List`
String get numberedList {
return Intl.message(
'Numbered List',
name: 'numberedList',
desc: '',
args: [],
);
}
/// `Quote`
String get quote {
return Intl.message(
'Quote',
name: 'quote',
desc: '',
args: [],
);
}
/// `Strikethrough`
String get strikethrough {
return Intl.message(
'Strikethrough',
name: 'strikethrough',
desc: '',
args: [],
);
}
/// `Text`
String get text {
return Intl.message(
'Text',
name: 'text',
desc: '',
args: [],
);
}
/// `Underline`
String get underline {
return Intl.message(
'Underline',
name: 'underline',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate
extends LocalizationsDelegate<AppFlowyEditorLocalizations> {
const AppLocalizationDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'ca'),
Locale.fromSubtags(languageCode: 'de', countryCode: 'DE'),
Locale.fromSubtags(languageCode: 'es', countryCode: 'VE'),
Locale.fromSubtags(languageCode: 'fr', countryCode: 'CA'),
Locale.fromSubtags(languageCode: 'fr', countryCode: 'FR'),
Locale.fromSubtags(languageCode: 'hu', countryCode: 'HU'),
Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'),
Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'),
Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'),
Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'),
Locale.fromSubtags(languageCode: 'pt', countryCode: 'PT'),
Locale.fromSubtags(languageCode: 'ru', countryCode: 'RU'),
Locale.fromSubtags(languageCode: 'tr', countryCode: 'TR'),
Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'),
Locale.fromSubtags(languageCode: 'zh', countryCode: 'TW'),
];
}
@override
bool isSupported(Locale locale) => _isSupported(locale);
@override
Future<AppFlowyEditorLocalizations> load(Locale locale) =>
AppFlowyEditorLocalizations.load(locale);
@override
bool shouldReload(AppLocalizationDelegate old) => false;
bool _isSupported(Locale locale) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,61 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
abstract class BuiltInTextWidget extends StatefulWidget {
const BuiltInTextWidget({
Key? key,
}) : super(key: key);
EditorState get editorState;
TextNode get textNode;
}
mixin BuiltInStyleMixin<T extends BuiltInTextWidget> on State<T> {
EdgeInsets get padding {
final padding = widget.editorState.editorStyle.style(
widget.editorState,
widget.textNode,
'padding',
);
if (padding is EdgeInsets) {
return padding;
}
return const EdgeInsets.all(0);
}
TextStyle get textStyle {
final textStyle = widget.editorState.editorStyle.style(
widget.editorState,
widget.textNode,
'textStyle',
);
if (textStyle is TextStyle) {
return textStyle;
}
return const TextStyle();
}
Size? get iconSize {
final iconSize = widget.editorState.editorStyle.style(
widget.editorState,
widget.textNode,
'iconSize',
);
if (iconSize is Size) {
return iconSize;
}
return const Size.square(18.0);
}
EdgeInsets? get iconPadding {
final iconPadding = widget.editorState.editorStyle.style(
widget.editorState,
widget.textNode,
'iconPadding',
);
if (iconPadding is EdgeInsets) {
return iconPadding;
}
return const EdgeInsets.all(0);
}
}

View File

@ -1,9 +1,9 @@
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
@ -24,14 +24,16 @@ class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
});
}
class BulletedListTextNodeWidget extends StatefulWidget {
class BulletedListTextNodeWidget extends BuiltInTextWidget {
const BulletedListTextNodeWidget({
Key? key,
required this.textNode,
required this.editorState,
}) : super(key: key);
@override
final TextNode textNode;
@override
final EditorState editorState;
@override
@ -42,36 +44,40 @@ class BulletedListTextNodeWidget extends StatefulWidget {
// customize
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
with SelectableMixin, DefaultSelectable {
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
@override
final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text');
final _iconWidth = 20.0;
final _iconRightPadding = 5.0;
@override
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return super.baseOffset.translate(0, padding.top);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: defaultLinePadding),
padding: padding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowySvg(
key: iconKey,
width: _iconWidth,
height: _iconWidth,
padding: EdgeInsets.only(right: _iconRightPadding),
width: iconSize?.width,
height: iconSize?.height,
padding: iconPadding,
name: 'point',
),
Flexible(
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'List',
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
textNode: widget.textNode,
editorState: widget.editorState,
),

View File

@ -1,12 +1,16 @@
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
import 'package:flutter/material.dart';
class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@ -21,18 +25,20 @@ class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@override
NodeValidator<Node> get nodeValidator => ((node) {
return node.attributes.containsKey(StyleKey.checkbox);
return node.attributes.containsKey(BuiltInAttributeKey.checkbox);
});
}
class CheckboxNodeWidget extends StatefulWidget {
class CheckboxNodeWidget extends BuiltInTextWidget {
const CheckboxNodeWidget({
Key? key,
required this.textNode,
required this.editorState,
}) : super(key: key);
@override
final TextNode textNode;
@override
final EditorState editorState;
@override
@ -40,18 +46,21 @@ class CheckboxNodeWidget extends StatefulWidget {
}
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
with SelectableMixin, DefaultSelectable {
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
@override
final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'checkbox_text');
final _iconWidth = 20.0;
final _iconRightPadding = 5.0;
@override
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return super.baseOffset.translate(0, padding.top);
}
@override
Widget build(BuildContext context) {
if (widget.textNode.children.isEmpty) {
@ -64,33 +73,32 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
Widget _buildWithSingle(BuildContext context) {
final check = widget.textNode.attributes.check;
return Padding(
padding: EdgeInsets.only(bottom: defaultLinePadding),
padding: padding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
key: iconKey,
child: FlowySvg(
width: _iconWidth,
height: _iconWidth,
padding: EdgeInsets.only(right: _iconRightPadding),
width: iconSize?.width,
height: iconSize?.height,
padding: iconPadding,
name: check ? 'check' : 'uncheck',
),
onTap: () {
TransactionBuilder(widget.editorState)
..updateNode(widget.textNode, {
StyleKey.checkbox: !check,
})
..commit();
formatCheckbox(widget.editorState, !check);
},
),
Flexible(
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'To-do',
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
textNode: widget.textNode,
textSpanDecorator: _textSpanDecorator,
placeholderTextSpanDecorator: _textSpanDecorator,
textSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
editorState: widget.editorState,
),
),
@ -134,28 +142,4 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
],
);
}
TextSpan _textSpanDecorator(TextSpan textSpan) {
return TextSpan(
children: textSpan.children
?.whereType<TextSpan>()
.map(
(span) => TextSpan(
text: span.text,
style: widget.textNode.attributes.check
? span.style?.copyWith(
color: Colors.grey,
decoration: TextDecoration.combine([
TextDecoration.lineThrough,
if (span.style?.decoration != null)
span.style!.decoration!
]),
)
: span.style,
recognizer: span.recognizer,
),
)
.toList(),
);
}
}

View File

@ -1,8 +1,6 @@
import 'dart:async';
import 'dart:ui';
import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@ -13,8 +11,12 @@ import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart';
import 'package:appflowy_editor/src/document/text_delta.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan);
@ -23,6 +25,7 @@ class FlowyRichText extends StatefulWidget {
Key? key,
this.cursorHeight,
this.cursorWidth = 1.0,
this.lineHeight = 1.0,
this.textSpanDecorator,
this.placeholderText = ' ',
this.placeholderTextSpanDecorator,
@ -34,6 +37,7 @@ class FlowyRichText extends StatefulWidget {
final EditorState editorState;
final double? cursorHeight;
final double cursorWidth;
final double lineHeight;
final FlowyTextSpanDecorator? textSpanDecorator;
final String placeholderText;
final FlowyTextSpanDecorator? placeholderTextSpanDecorator;
@ -46,8 +50,6 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
var _textKey = GlobalKey();
final _placeholderTextKey = GlobalKey();
final _lineHeight = 1.5;
RenderParagraph get _renderParagraph =>
_textKey.currentContext?.findRenderObject() as RenderParagraph;
@ -90,20 +92,6 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
cursorOffset = _placeholderRenderParagraph.getOffsetForCaret(
textPosition, Rect.zero);
}
if (cursorHeight != null) {
// workaround: Calling the `getFullHeightForCaret` function will return
// the full height of rich text component instead of the plain text
// if we set the line height.
// So need to divide by the line height to get the expected value.
//
// And the default height of plain text is too short. Add a magic height
// to expand it.
const magicHeight = 3.0;
cursorOffset = cursorOffset.translate(
0, (cursorHeight - cursorHeight / _lineHeight) / 2.0);
cursorHeight /= _lineHeight;
cursorHeight += magicHeight;
}
final rect = Rect.fromLTWH(
cursorOffset.dx - (widget.cursorWidth / 2),
cursorOffset.dy,
@ -190,8 +178,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
key: _placeholderTextKey,
textHeightBehavior: const TextHeightBehavior(
applyHeightToFirstAscent: false, applyHeightToLastDescent: false),
text: widget.textSpanDecorator != null
? widget.textSpanDecorator!(textSpan)
text: widget.placeholderTextSpanDecorator != null
? widget.placeholderTextSpanDecorator!(textSpan)
: textSpan,
);
}
@ -210,42 +198,73 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
);
}
TextSpan get _textSpan {
var offset = 0;
TextSpan get _placeholderTextSpan {
final style = widget.editorState.editorStyle.textStyle;
return TextSpan(
children: widget.textNode.delta.whereType<TextInsert>().map((insert) {
GestureRecognizer? gestureRecognizer;
if (insert.attributes?[StyleKey.href] != null) {
gestureRecognizer = _buildTapHrefGestureRecognizer(
insert.attributes![StyleKey.href],
Selection.single(
path: widget.textNode.path,
startOffset: offset,
endOffset: offset + insert.length,
),
);
}
offset += insert.length;
final textSpan = RichTextStyle(
attributes: insert.attributes ?? {},
text: insert.content,
height: _lineHeight,
gestureRecognizer: gestureRecognizer,
).toTextSpan();
return textSpan;
}).toList(growable: false),
children: [
TextSpan(
text: widget.placeholderText,
style: style.defaultPlaceholderTextStyle,
),
],
);
}
TextSpan get _placeholderTextSpan => TextSpan(children: [
RichTextStyle(
text: widget.placeholderText,
attributes: {
StyleKey.color: '0xFF707070',
},
height: _lineHeight,
).toTextSpan()
]);
TextSpan get _textSpan {
var offset = 0;
List<TextSpan> textSpans = [];
final style = widget.editorState.editorStyle.textStyle;
final textInserts = widget.textNode.delta.whereType<TextInsert>();
for (final textInsert in textInserts) {
var textStyle = style.defaultTextStyle;
GestureRecognizer? recognizer;
final attributes = textInsert.attributes;
if (attributes != null) {
if (attributes.bold == true) {
textStyle = textStyle.combine(style.bold);
}
if (attributes.italic == true) {
textStyle = textStyle.combine(style.italic);
}
if (attributes.underline == true) {
textStyle = textStyle.combine(style.underline);
}
if (attributes.strikethrough == true) {
textStyle = textStyle.combine(style.strikethrough);
}
if (attributes.href != null) {
textStyle = textStyle.combine(style.href);
recognizer = _buildTapHrefGestureRecognizer(
attributes.href!,
Selection.single(
path: widget.textNode.path,
startOffset: offset,
endOffset: offset + textInsert.length,
),
);
}
if (attributes.code == true) {
textStyle = textStyle.combine(style.code);
}
if (attributes.backgroundColor != null) {
textStyle = textStyle.combine(
TextStyle(backgroundColor: attributes.backgroundColor),
);
}
}
offset += textInsert.length;
textSpans.add(
TextSpan(
text: textInsert.content,
style: textStyle,
recognizer: recognizer,
),
);
}
return TextSpan(
children: textSpans,
);
}
GestureRecognizer _buildTapHrefGestureRecognizer(
String href, Selection selection) {

View File

@ -1,11 +1,13 @@
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@override
@ -23,14 +25,16 @@ class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
});
}
class HeadingTextNodeWidget extends StatefulWidget {
class HeadingTextNodeWidget extends BuiltInTextWidget {
const HeadingTextNodeWidget({
Key? key,
required this.textNode,
required this.editorState,
}) : super(key: key);
@override
final TextNode textNode;
@override
final EditorState editorState;
@override
@ -39,12 +43,11 @@ class HeadingTextNodeWidget extends StatefulWidget {
// customize
class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
with SelectableMixin, DefaultSelectable {
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
@override
GlobalKey? get iconKey => null;
final _richTextKey = GlobalKey(debugLabel: 'heading_text');
final _topPadding = 5.0;
@override
SelectableMixin<StatefulWidget> get forward =>
@ -52,58 +55,23 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
@override
Offset get baseOffset {
return Offset(0, _topPadding);
return padding.topLeft;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
top: _topPadding,
bottom: defaultLinePadding,
),
padding: padding,
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'Heading',
placeholderTextSpanDecorator: _placeholderTextSpanDecorator,
textSpanDecorator: _textSpanDecorator,
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
textNode: widget.textNode,
editorState: widget.editorState,
),
);
}
TextSpan _textSpanDecorator(TextSpan textSpan) {
return TextSpan(
children: textSpan.children
?.whereType<TextSpan>()
.map(
(span) => TextSpan(
text: span.text,
style: span.style?.copyWith(
fontSize: widget.textNode.attributes.fontSize,
),
recognizer: span.recognizer,
),
)
.toList(),
);
}
TextSpan _placeholderTextSpanDecorator(TextSpan textSpan) {
return TextSpan(
children: textSpan.children
?.whereType<TextSpan>()
.map(
(span) => TextSpan(
text: span.text,
style: span.style?.copyWith(
fontSize: widget.textNode.attributes.fontSize,
),
recognizer: span.recognizer,
),
)
.toList(),
);
}
}

View File

@ -1,12 +1,13 @@
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@override
@ -24,14 +25,16 @@ class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
});
}
class NumberListTextNodeWidget extends StatefulWidget {
class NumberListTextNodeWidget extends BuiltInTextWidget {
const NumberListTextNodeWidget({
Key? key,
required this.textNode,
required this.editorState,
}) : super(key: key);
@override
final TextNode textNode;
@override
final EditorState editorState;
@override
@ -39,11 +42,8 @@ class NumberListTextNodeWidget extends StatefulWidget {
_NumberListTextNodeWidgetState();
}
// customize
const double _numberHorizontalPadding = 8;
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
with SelectableMixin, DefaultSelectable {
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
@override
final iconKey = GlobalKey();
@ -53,31 +53,42 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return super.baseOffset.translate(0, padding.top);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: defaultLinePadding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
key: iconKey,
padding: const EdgeInsets.symmetric(
horizontal: _numberHorizontalPadding, vertical: 0),
child: Text(
'${widget.textNode.attributes.number.toString()}.',
style: const TextStyle(fontSize: 16),
),
padding: padding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
key: iconKey,
padding: iconPadding,
child: Text(
'${widget.textNode.attributes.number.toString()}.',
// FIXME: customize
style: const TextStyle(fontSize: 16.0, color: Colors.black),
),
Flexible(
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'List',
textNode: widget.textNode,
editorState: widget.editorState,
),
),
Flexible(
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'List',
textNode: widget.textNode,
editorState: widget.editorState,
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
textSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
),
],
));
),
],
),
);
}
}

View File

@ -1,9 +1,9 @@
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
@ -24,14 +24,16 @@ class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
});
}
class QuotedTextNodeWidget extends StatefulWidget {
class QuotedTextNodeWidget extends BuiltInTextWidget {
const QuotedTextNodeWidget({
Key? key,
required this.textNode,
required this.editorState,
}) : super(key: key);
@override
final TextNode textNode;
@override
final EditorState editorState;
@override
@ -41,30 +43,33 @@ class QuotedTextNodeWidget extends StatefulWidget {
// customize
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
with SelectableMixin, DefaultSelectable {
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
@override
final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'quoted_text');
final _iconWidth = 20.0;
final _iconRightPadding = 5.0;
@override
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return super.baseOffset.translate(0, padding.top);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: defaultLinePadding),
padding: padding,
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FlowySvg(
key: iconKey,
width: _iconWidth,
padding: EdgeInsets.only(right: _iconRightPadding),
width: iconSize?.width,
padding: iconPadding,
name: 'quote',
),
Flexible(
@ -72,6 +77,7 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
key: _richTextKey,
placeholderText: 'Quote',
textNode: widget.textNode,
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
editorState: widget.editorState,
),
),

View File

@ -1,8 +1,8 @@
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
@ -23,14 +23,16 @@ class RichTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
});
}
class RichTextNodeWidget extends StatefulWidget {
class RichTextNodeWidget extends BuiltInTextWidget {
const RichTextNodeWidget({
Key? key,
required this.textNode,
required this.editorState,
}) : super(key: key);
@override
final TextNode textNode;
@override
final EditorState editorState;
@override
@ -40,7 +42,7 @@ class RichTextNodeWidget extends StatefulWidget {
// customize
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
with SelectableMixin, DefaultSelectable {
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
@override
GlobalKey? get iconKey => null;
@ -50,13 +52,19 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
Offset get baseOffset {
return padding.topLeft;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: defaultLinePadding),
padding: padding,
child: FlowyRichText(
key: _richTextKey,
textNode: widget.textNode,
lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
editorState: widget.editorState,
),
);

View File

@ -1,282 +0,0 @@
import 'package:appflowy_editor/src/document/attributes.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
///
/// Supported partial rendering types:
/// bold, italic,
/// underline, strikethrough,
/// color, font,
/// href
///
/// Supported global rendering types:
/// heading: h1, h2, h3, h4, h5, h6, ...
/// block quote,
/// list: ordered list, bulleted list,
/// code block
///
class StyleKey {
static String bold = 'bold';
static String italic = 'italic';
static String underline = 'underline';
static String strikethrough = 'strikethrough';
static String color = 'color';
static String backgroundColor = 'backgroundColor';
static String font = 'font';
static String href = 'href';
static String subtype = 'subtype';
static String heading = 'heading';
static String h1 = 'h1';
static String h2 = 'h2';
static String h3 = 'h3';
static String h4 = 'h4';
static String h5 = 'h5';
static String h6 = 'h6';
static String bulletedList = 'bulleted-list';
static String numberList = 'number-list';
static String quote = 'quote';
static String checkbox = 'checkbox';
static String code = 'code';
static String number = 'number';
static List<String> partialStyleKeys = [
StyleKey.bold,
StyleKey.italic,
StyleKey.underline,
StyleKey.strikethrough,
StyleKey.backgroundColor,
StyleKey.href,
StyleKey.code,
];
static List<String> globalStyleKeys = [
StyleKey.subtype,
StyleKey.heading,
StyleKey.checkbox,
StyleKey.bulletedList,
StyleKey.numberList,
StyleKey.quote,
];
}
// TODO: customize
double defaultLinePadding = 8.0;
double baseFontSize = 16.0;
String defaultHighlightColor = '0x6000BCF0';
String defaultBackgroundColor = '0x00000000';
// TODO: customize.
Map<String, double> headingToFontSize = {
StyleKey.h1: baseFontSize + 15,
StyleKey.h2: baseFontSize + 12,
StyleKey.h3: baseFontSize + 9,
StyleKey.h4: baseFontSize + 6,
StyleKey.h5: baseFontSize + 3,
StyleKey.h6: baseFontSize,
};
extension NodeAttributesExtensions on Attributes {
String? get heading {
if (containsKey(StyleKey.subtype) &&
containsKey(StyleKey.heading) &&
this[StyleKey.subtype] == StyleKey.heading &&
this[StyleKey.heading] is String) {
return this[StyleKey.heading];
}
return null;
}
double get fontSize {
if (heading != null) {
return headingToFontSize[heading]!;
}
return baseFontSize;
}
bool get quote {
return containsKey(StyleKey.quote);
}
Color? get quoteColor {
if (quote) {
return Colors.grey;
}
return null;
}
int? get number {
if (containsKey(StyleKey.number) && this[StyleKey.number] is int) {
return this[StyleKey.number];
}
return null;
}
bool get code {
if (containsKey(StyleKey.code) && this[StyleKey.code] == true) {
return this[StyleKey.code];
}
return false;
}
bool get check {
if (containsKey(StyleKey.checkbox) && this[StyleKey.checkbox] is bool) {
return this[StyleKey.checkbox];
}
return false;
}
}
extension DeltaAttributesExtensions on Attributes {
bool get bold {
return (containsKey(StyleKey.bold) && this[StyleKey.bold] == true);
}
bool get italic {
return (containsKey(StyleKey.italic) && this[StyleKey.italic] == true);
}
bool get underline {
return (containsKey(StyleKey.underline) &&
this[StyleKey.underline] == true);
}
bool get strikethrough {
return (containsKey(StyleKey.strikethrough) &&
this[StyleKey.strikethrough] == true);
}
Color? get color {
if (containsKey(StyleKey.color) && this[StyleKey.color] is String) {
return Color(
int.parse(this[StyleKey.color]),
);
}
return null;
}
Color? get backgroundColor {
if (containsKey(StyleKey.backgroundColor) &&
this[StyleKey.backgroundColor] is String) {
return Color(
int.parse(this[StyleKey.backgroundColor]),
);
}
return null;
}
String? get font {
// TODO: unspport now.
return null;
}
String? get href {
if (containsKey(StyleKey.href) && this[StyleKey.href] is String) {
return this[StyleKey.href];
}
return null;
}
}
class RichTextStyle {
// TODO: customize
RichTextStyle({
required this.attributes,
required this.text,
this.gestureRecognizer,
this.height = 1.5,
});
final Attributes attributes;
final String text;
final GestureRecognizer? gestureRecognizer;
final double height;
TextSpan toTextSpan() => _toTextSpan(height);
double get topPadding {
return 0;
}
TextSpan _toTextSpan(double? height) {
return TextSpan(
text: text,
recognizer: _recognizer,
style: TextStyle(
fontWeight: _fontWeight,
fontStyle: _fontStyle,
fontSize: _fontSize,
color: _textColor,
decoration: _textDecoration,
background: _background,
height: height,
),
);
}
Paint? get _background {
if (_backgroundColor != null) {
return Paint()
..color = _backgroundColor!
..strokeWidth = 24.0
..style = PaintingStyle.fill
..strokeJoin = StrokeJoin.round;
}
return null;
}
// bold
FontWeight get _fontWeight {
if (attributes.bold) {
return FontWeight.bold;
}
return FontWeight.normal;
}
// underline or strikethrough
TextDecoration get _textDecoration {
var decorations = [TextDecoration.none];
if (attributes.underline || attributes.href != null) {
decorations.add(TextDecoration.underline);
}
if (attributes.strikethrough) {
decorations.add(TextDecoration.lineThrough);
}
return TextDecoration.combine(decorations);
}
// font
FontStyle get _fontStyle =>
attributes.italic ? FontStyle.italic : FontStyle.normal;
// text color
Color get _textColor {
if (attributes.href != null) {
return Colors.lightBlue;
}
if (attributes.code) {
return Colors.lightBlue.withOpacity(0.8);
}
return attributes.color ?? Colors.black;
}
Color? get _backgroundColor {
if (attributes.backgroundColor != null) {
return attributes.backgroundColor!;
} else if (attributes.code) {
return Colors.blue.shade300.withOpacity(0.3);
}
return null;
}
// font size
double get _fontSize {
return baseFontSize;
}
// recognizer
GestureRecognizer? get _recognizer {
return gestureRecognizer;
}
}

View File

@ -1,10 +1,12 @@
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/l10n/l10n.dart';
import 'package:appflowy_editor/src/render/image/image_upload_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
abstract class SelectionMenuService {
Offset get topLeft;
@ -54,7 +56,13 @@ class SelectionMenu implements SelectionMenuService {
if (selectionRects.isEmpty) {
return;
}
final offset = selectionRects.first.bottomRight + const Offset(10, 10);
// Workaround: We can customize the padding through the [EditorStyle],
// but the coordinates of overlay are not properly converted currently.
// Just subtract the padding here as a result.
final baseOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final offset =
selectionRects.first.bottomRight + const Offset(10, 10) - baseOffset;
_topLeft = offset;
_selectionMenuEntry = OverlayEntry(builder: (context) {
@ -116,7 +124,7 @@ List<SelectionMenuItem> get defaultSelectionMenuItems =>
_defaultSelectionMenuItems;
final List<SelectionMenuItem> _defaultSelectionMenuItems = [
SelectionMenuItem(
name: 'Text',
name: AppFlowyEditorLocalizations.current.text,
icon: _selectionMenuIcon('text'),
keywords: ['text'],
handler: (editorState, _, __) {
@ -124,37 +132,37 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
},
),
SelectionMenuItem(
name: 'Heading 1',
name: AppFlowyEditorLocalizations.current.heading1,
icon: _selectionMenuIcon('h1'),
keywords: ['heading 1, h1'],
handler: (editorState, _, __) {
insertHeadingAfterSelection(editorState, StyleKey.h1);
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h1);
},
),
SelectionMenuItem(
name: 'Heading 2',
name: AppFlowyEditorLocalizations.current.heading2,
icon: _selectionMenuIcon('h2'),
keywords: ['heading 2, h2'],
handler: (editorState, _, __) {
insertHeadingAfterSelection(editorState, StyleKey.h2);
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h2);
},
),
SelectionMenuItem(
name: 'Heading 3',
name: AppFlowyEditorLocalizations.current.heading3,
icon: _selectionMenuIcon('h3'),
keywords: ['heading 3, h3'],
handler: (editorState, _, __) {
insertHeadingAfterSelection(editorState, StyleKey.h3);
insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h3);
},
),
SelectionMenuItem(
name: 'Image',
name: AppFlowyEditorLocalizations.current.image,
icon: _selectionMenuIcon('image'),
keywords: ['image'],
handler: showImageUploadMenu,
),
SelectionMenuItem(
name: 'Bulleted list',
name: AppFlowyEditorLocalizations.current.bulletedList,
icon: _selectionMenuIcon('bulleted_list'),
keywords: ['bulleted list', 'list', 'unordered list'],
handler: (editorState, _, __) {
@ -162,7 +170,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
},
),
SelectionMenuItem(
name: 'Checkbox',
name: AppFlowyEditorLocalizations.current.checkbox,
icon: _selectionMenuIcon('checkbox'),
keywords: ['todo list', 'list', 'checkbox list'],
handler: (editorState, _, __) {
@ -170,7 +178,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
},
),
SelectionMenuItem(
name: 'Quote',
name: AppFlowyEditorLocalizations.current.quote,
icon: _selectionMenuIcon('quote'),
keywords: ['quote', 'refer'],
handler: (editorState, _, __) {

View File

@ -1,20 +1,254 @@
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
typedef PluginStyler = Object Function(EditorState editorState, Node node);
typedef PluginStyle = Map<String, PluginStyler>;
/// Editor style configuration
class EditorStyle {
const EditorStyle({
EditorStyle({
required this.padding,
});
required this.textStyle,
required this.cursorColor,
required this.selectionColor,
Map<String, PluginStyle> pluginStyles = const {},
}) {
_pluginStyles.addAll(pluginStyles);
}
const EditorStyle.defaultStyle()
: padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0);
EditorStyle.defaultStyle()
: padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
textStyle = BuiltInTextStyle.builtIn(),
cursorColor = const Color(0xFF00BCF0),
selectionColor = const Color.fromARGB(53, 111, 201, 231);
/// The margin of the document context from the editor.
final EdgeInsets padding;
final BuiltInTextStyle textStyle;
final Color cursorColor;
final Color selectionColor;
EditorStyle copyWith({EdgeInsets? padding}) {
final Map<String, PluginStyle> _pluginStyles = Map.from(builtInTextStylers);
Object? style(EditorState editorState, Node node, String key) {
final styler = _pluginStyles[node.id]?[key];
if (styler != null) {
return styler(editorState, node);
}
return null;
}
EditorStyle copyWith({
EdgeInsets? padding,
BuiltInTextStyle? textStyle,
Color? cursorColor,
Color? selectionColor,
Map<String, PluginStyle>? pluginStyles,
}) {
return EditorStyle(
padding: padding ?? this.padding,
textStyle: textStyle ?? this.textStyle,
cursorColor: cursorColor ?? this.cursorColor,
selectionColor: selectionColor ?? this.selectionColor,
pluginStyles: pluginStyles ?? {},
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is EditorStyle &&
other.padding == padding &&
other.textStyle == textStyle &&
other.cursorColor == cursorColor &&
other.selectionColor == selectionColor;
}
@override
int get hashCode {
return padding.hashCode ^
textStyle.hashCode ^
cursorColor.hashCode ^
selectionColor.hashCode;
}
}
PluginStyle get builtInPluginStyle => Map.from({
'padding': (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
'textStyle': (_, __) => const TextStyle(),
'iconSize': (_, __) => const Size.square(20.0),
'iconPadding': (_, __) => const EdgeInsets.only(right: 5.0),
});
Map<String, PluginStyle> builtInTextStylers = {
'text': builtInPluginStyle,
'text/checkbox': builtInPluginStyle
..update(
'textStyle',
(_) => (EditorState editorState, Node node) {
if (node is TextNode && node.attributes.check == true) {
return const TextStyle(
color: Colors.grey,
decoration: TextDecoration.lineThrough,
);
}
return const TextStyle();
},
),
'text/heading': builtInPluginStyle
..update(
'textStyle',
(_) => (EditorState editorState, Node node) {
final headingToFontSize = {
'h1': 32.0,
'h2': 28.0,
'h3': 24.0,
'h4': 18.0,
'h5': 18.0,
'h6': 18.0,
};
final fontSize = headingToFontSize[node.attributes.heading] ?? 18.0;
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold);
},
),
'text/bulleted-list': builtInPluginStyle,
'text/number-list': builtInPluginStyle
..update(
'iconPadding',
(_) => (EditorState editorState, Node node) {
return const EdgeInsets.only(left: 5.0, right: 5.0);
},
),
'text/quote': builtInPluginStyle,
'image': builtInPluginStyle,
};
class BuiltInTextStyle {
const BuiltInTextStyle({
required this.defaultTextStyle,
required this.defaultPlaceholderTextStyle,
required this.bold,
required this.italic,
required this.underline,
required this.strikethrough,
required this.href,
required this.code,
this.highlightColorHex = '0x6000BCF0',
this.lineHeight = 1.5,
});
final TextStyle defaultTextStyle;
final TextStyle defaultPlaceholderTextStyle;
final TextStyle bold;
final TextStyle italic;
final TextStyle underline;
final TextStyle strikethrough;
final TextStyle href;
final TextStyle code;
final String highlightColorHex;
final double lineHeight;
BuiltInTextStyle.builtIn()
: defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.black),
defaultPlaceholderTextStyle =
const TextStyle(fontSize: 16.0, color: Colors.grey),
bold = const TextStyle(fontWeight: FontWeight.bold),
italic = const TextStyle(fontStyle: FontStyle.italic),
underline = const TextStyle(decoration: TextDecoration.underline),
strikethrough = const TextStyle(decoration: TextDecoration.lineThrough),
href = const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
code = const TextStyle(
fontFamily: 'monospace',
color: Color(0xFF00BCF0),
backgroundColor: Color(0xFFE0F8FF),
),
highlightColorHex = '0x6000BCF0',
lineHeight = 1.5;
BuiltInTextStyle.builtInDarkMode()
: defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.white),
defaultPlaceholderTextStyle = TextStyle(
fontSize: 16.0,
color: Colors.white.withOpacity(0.3),
),
bold = const TextStyle(fontWeight: FontWeight.bold),
italic = const TextStyle(fontStyle: FontStyle.italic),
underline = const TextStyle(decoration: TextDecoration.underline),
strikethrough = const TextStyle(decoration: TextDecoration.lineThrough),
href = const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
code = const TextStyle(
fontFamily: 'monospace',
color: Color(0xFF00BCF0),
backgroundColor: Color(0xFFE0F8FF),
),
highlightColorHex = '0x6000BCF0',
lineHeight = 1.5;
BuiltInTextStyle copyWith({
TextStyle? defaultTextStyle,
TextStyle? defaultPlaceholderTextStyle,
TextStyle? bold,
TextStyle? italic,
TextStyle? underline,
TextStyle? strikethrough,
TextStyle? href,
TextStyle? code,
String? highlightColorHex,
double? lineHeight,
}) {
return BuiltInTextStyle(
defaultTextStyle: defaultTextStyle ?? this.defaultTextStyle,
defaultPlaceholderTextStyle:
defaultPlaceholderTextStyle ?? this.defaultPlaceholderTextStyle,
bold: bold ?? this.bold,
italic: italic ?? this.italic,
underline: underline ?? this.underline,
strikethrough: strikethrough ?? this.strikethrough,
href: href ?? this.href,
code: code ?? this.code,
highlightColorHex: highlightColorHex ?? this.highlightColorHex,
lineHeight: lineHeight ?? this.lineHeight,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is BuiltInTextStyle &&
other.defaultTextStyle == defaultTextStyle &&
other.defaultPlaceholderTextStyle == defaultPlaceholderTextStyle &&
other.bold == bold &&
other.italic == italic &&
other.underline == underline &&
other.strikethrough == strikethrough &&
other.href == href &&
other.code == code &&
other.highlightColorHex == highlightColorHex &&
other.lineHeight == lineHeight;
}
@override
int get hashCode {
return defaultTextStyle.hashCode ^
defaultPlaceholderTextStyle.hashCode ^
bold.hashCode ^
italic.hashCode ^
underline.hashCode ^
strikethrough.hashCode ^
href.hashCode ^
code.hashCode ^
highlightColorHex.hashCode ^
lineHeight.hashCode;
}
}

View File

@ -2,12 +2,13 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart';
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
import 'package:flutter/material.dart';
import 'package:rich_clipboard/rich_clipboard.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
typedef ToolbarItemEventHandler = void Function(
EditorState editorState, BuildContext context);
@ -63,7 +64,7 @@ List<ToolbarItem> defaultToolbarItems = [
ToolbarItem(
id: 'appflowy.toolbar.h1',
type: 1,
tooltipsMessage: 'Heading 1',
tooltipsMessage: AppFlowyEditorLocalizations.current.heading1,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/h1',
color: isHighlight ? Colors.lightBlue : null,
@ -71,15 +72,16 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _onlyShowInSingleTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.heading,
(value) => value == StyleKey.h1,
BuiltInAttributeKey.heading,
(value) => value == BuiltInAttributeKey.h1,
),
handler: (editorState, context) => formatHeading(editorState, StyleKey.h1),
handler: (editorState, context) =>
formatHeading(editorState, BuiltInAttributeKey.h1),
),
ToolbarItem(
id: 'appflowy.toolbar.h2',
type: 1,
tooltipsMessage: 'Heading 2',
tooltipsMessage: AppFlowyEditorLocalizations.current.heading2,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/h2',
color: isHighlight ? Colors.lightBlue : null,
@ -87,15 +89,16 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _onlyShowInSingleTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.heading,
(value) => value == StyleKey.h2,
BuiltInAttributeKey.heading,
(value) => value == BuiltInAttributeKey.h2,
),
handler: (editorState, context) => formatHeading(editorState, StyleKey.h2),
handler: (editorState, context) =>
formatHeading(editorState, BuiltInAttributeKey.h2),
),
ToolbarItem(
id: 'appflowy.toolbar.h3',
type: 1,
tooltipsMessage: 'Heading 3',
tooltipsMessage: AppFlowyEditorLocalizations.current.heading3,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/h3',
color: isHighlight ? Colors.lightBlue : null,
@ -103,15 +106,16 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _onlyShowInSingleTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.heading,
(value) => value == StyleKey.h3,
BuiltInAttributeKey.heading,
(value) => value == BuiltInAttributeKey.h3,
),
handler: (editorState, context) => formatHeading(editorState, StyleKey.h3),
handler: (editorState, context) =>
formatHeading(editorState, BuiltInAttributeKey.h3),
),
ToolbarItem(
id: 'appflowy.toolbar.bold',
type: 2,
tooltipsMessage: 'Bold',
tooltipsMessage: AppFlowyEditorLocalizations.current.bold,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/bold',
color: isHighlight ? Colors.lightBlue : null,
@ -119,7 +123,7 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _showInTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.bold,
BuiltInAttributeKey.bold,
(value) => value == true,
),
handler: (editorState, context) => formatBold(editorState),
@ -127,7 +131,7 @@ List<ToolbarItem> defaultToolbarItems = [
ToolbarItem(
id: 'appflowy.toolbar.italic',
type: 2,
tooltipsMessage: 'Italic',
tooltipsMessage: AppFlowyEditorLocalizations.current.italic,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/italic',
color: isHighlight ? Colors.lightBlue : null,
@ -135,7 +139,7 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _showInTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.italic,
BuiltInAttributeKey.italic,
(value) => value == true,
),
handler: (editorState, context) => formatItalic(editorState),
@ -143,7 +147,7 @@ List<ToolbarItem> defaultToolbarItems = [
ToolbarItem(
id: 'appflowy.toolbar.underline',
type: 2,
tooltipsMessage: 'Underline',
tooltipsMessage: AppFlowyEditorLocalizations.current.underline,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/underline',
color: isHighlight ? Colors.lightBlue : null,
@ -151,7 +155,7 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _showInTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.underline,
BuiltInAttributeKey.underline,
(value) => value == true,
),
handler: (editorState, context) => formatUnderline(editorState),
@ -159,7 +163,7 @@ List<ToolbarItem> defaultToolbarItems = [
ToolbarItem(
id: 'appflowy.toolbar.strikethrough',
type: 2,
tooltipsMessage: 'Strikethrough',
tooltipsMessage: AppFlowyEditorLocalizations.current.strikethrough,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/strikethrough',
color: isHighlight ? Colors.lightBlue : null,
@ -167,7 +171,7 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _showInTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.strikethrough,
BuiltInAttributeKey.strikethrough,
(value) => value == true,
),
handler: (editorState, context) => formatStrikethrough(editorState),
@ -175,7 +179,7 @@ List<ToolbarItem> defaultToolbarItems = [
ToolbarItem(
id: 'appflowy.toolbar.code',
type: 2,
tooltipsMessage: 'Embed Code',
tooltipsMessage: AppFlowyEditorLocalizations.current.embedCode,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/code',
color: isHighlight ? Colors.lightBlue : null,
@ -183,15 +187,15 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _showInTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.code,
(value) => value == StyleKey.code,
BuiltInAttributeKey.code,
(value) => value == true,
),
handler: (editorState, context) => formatEmbedCode(editorState),
),
ToolbarItem(
id: 'appflowy.toolbar.quote',
type: 3,
tooltipsMessage: 'Quote',
tooltipsMessage: AppFlowyEditorLocalizations.current.quote,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/quote',
color: isHighlight ? Colors.lightBlue : null,
@ -199,15 +203,15 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _onlyShowInSingleTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.subtype,
(value) => value == StyleKey.quote,
BuiltInAttributeKey.subtype,
(value) => value == BuiltInAttributeKey.quote,
),
handler: (editorState, context) => formatQuote(editorState),
),
ToolbarItem(
id: 'appflowy.toolbar.bulleted_list',
type: 3,
tooltipsMessage: 'Bulleted list',
tooltipsMessage: AppFlowyEditorLocalizations.current.bulletedList,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/bulleted_list',
color: isHighlight ? Colors.lightBlue : null,
@ -215,15 +219,15 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _onlyShowInSingleTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.subtype,
(value) => value == StyleKey.bulletedList,
BuiltInAttributeKey.subtype,
(value) => value == BuiltInAttributeKey.bulletedList,
),
handler: (editorState, context) => formatBulletedList(editorState),
),
ToolbarItem(
id: 'appflowy.toolbar.link',
type: 4,
tooltipsMessage: 'Link',
tooltipsMessage: AppFlowyEditorLocalizations.current.link,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/link',
color: isHighlight ? Colors.lightBlue : null,
@ -231,7 +235,7 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _onlyShowInSingleTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.href,
BuiltInAttributeKey.href,
(value) => value != null,
),
handler: (editorState, context) => showLinkMenu(context, editorState),
@ -239,7 +243,7 @@ List<ToolbarItem> defaultToolbarItems = [
ToolbarItem(
id: 'appflowy.toolbar.highlight',
type: 4,
tooltipsMessage: 'Highlight',
tooltipsMessage: AppFlowyEditorLocalizations.current.highlight,
iconBuilder: (isHighlight) => FlowySvg(
name: 'toolbar/highlight',
color: isHighlight ? Colors.lightBlue : null,
@ -247,10 +251,13 @@ List<ToolbarItem> defaultToolbarItems = [
validator: _showInTextSelection,
highlightCallback: (editorState) => _allSatisfy(
editorState,
StyleKey.backgroundColor,
BuiltInAttributeKey.backgroundColor,
(value) => value != null,
),
handler: (editorState, context) => formatHighlight(editorState),
handler: (editorState, context) => formatHighlight(
editorState,
editorState.editorStyle.textStyle.highlightColorHex,
),
),
];
@ -296,6 +303,9 @@ void showLinkMenu(
matchRect = rect;
}
}
final baseOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
matchRect = matchRect.shift(-baseOffset);
_dismissLinkMenu();
_editorState = editorState;
@ -314,7 +324,8 @@ void showLinkMenu(
final textNode = node.first as TextNode;
String? linkText;
if (textNode.allSatisfyLinkInSelection(selection)) {
linkText = textNode.getAttributeInSelection(selection, StyleKey.href);
linkText =
textNode.getAttributeInSelection(selection, BuiltInAttributeKey.href);
}
_linkMenuOverlay = OverlayEntry(builder: (context) {
return Positioned(
@ -328,7 +339,8 @@ void showLinkMenu(
},
onSubmitted: (text) {
TransactionBuilder(editorState)
..formatText(textNode, index, length, {StyleKey.href: text})
..formatText(
textNode, index, length, {BuiltInAttributeKey.href: text})
..commit();
_dismissLinkMenu();
},
@ -338,7 +350,8 @@ void showLinkMenu(
},
onRemoveLink: () {
TransactionBuilder(editorState)
..formatText(textNode, index, length, {StyleKey.href: null})
..formatText(
textNode, index, length, {BuiltInAttributeKey.href: null})
..commit();
_dismissLinkMenu();
},

View File

@ -6,31 +6,31 @@ import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
void insertHeadingAfterSelection(EditorState editorState, String heading) {
insertTextNodeAfterSelection(editorState, {
StyleKey.subtype: StyleKey.heading,
StyleKey.heading: heading,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: heading,
});
}
void insertQuoteAfterSelection(EditorState editorState) {
insertTextNodeAfterSelection(editorState, {
StyleKey.subtype: StyleKey.quote,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
});
}
void insertCheckboxAfterSelection(EditorState editorState) {
insertTextNodeAfterSelection(editorState, {
StyleKey.subtype: StyleKey.checkbox,
StyleKey.checkbox: false,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.checkbox: false,
});
}
void insertBulletedListAfterSelection(EditorState editorState) {
insertTextNodeAfterSelection(editorState, {
StyleKey.subtype: StyleKey.bulletedList,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
});
}
@ -68,27 +68,27 @@ void formatText(EditorState editorState) {
void formatHeading(EditorState editorState, String heading) {
formatTextNodes(editorState, {
StyleKey.subtype: StyleKey.heading,
StyleKey.heading: heading,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: heading,
});
}
void formatQuote(EditorState editorState) {
formatTextNodes(editorState, {
StyleKey.subtype: StyleKey.quote,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
});
}
void formatCheckbox(EditorState editorState) {
void formatCheckbox(EditorState editorState, bool check) {
formatTextNodes(editorState, {
StyleKey.subtype: StyleKey.checkbox,
StyleKey.checkbox: false,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.checkbox: check,
});
}
void formatBulletedList(EditorState editorState) {
formatTextNodes(editorState, {
StyleKey.subtype: StyleKey.bulletedList,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
});
}
@ -107,7 +107,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
..updateNode(
textNode,
Attributes.fromIterable(
StyleKey.globalStyleKeys,
BuiltInAttributeKey.globalStyleKeys,
value: (_) => null,
)..addAll(attributes),
)
@ -124,44 +124,58 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
}
bool formatBold(EditorState editorState) {
return formatRichTextPartialStyle(editorState, StyleKey.bold);
return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.bold);
}
bool formatItalic(EditorState editorState) {
return formatRichTextPartialStyle(editorState, StyleKey.italic);
return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.italic);
}
bool formatUnderline(EditorState editorState) {
return formatRichTextPartialStyle(editorState, StyleKey.underline);
return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.underline);
}
bool formatStrikethrough(EditorState editorState) {
return formatRichTextPartialStyle(editorState, StyleKey.strikethrough);
return formatRichTextPartialStyle(
editorState, BuiltInAttributeKey.strikethrough);
}
bool formatEmbedCode(EditorState editorState) {
return formatRichTextPartialStyle(editorState, StyleKey.code);
return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.code);
}
bool formatHighlight(EditorState editorState) {
bool formatHighlight(EditorState editorState, String colorHex) {
bool value = _allSatisfyInSelection(
editorState, StyleKey.backgroundColor, defaultHighlightColor);
return formatRichTextPartialStyle(editorState, StyleKey.backgroundColor,
customValue: value ? defaultBackgroundColor : defaultHighlightColor);
editorState,
BuiltInAttributeKey.backgroundColor,
colorHex,
);
return formatRichTextPartialStyle(
editorState,
BuiltInAttributeKey.backgroundColor,
customValue: value ? '0x00000000' : colorHex,
);
}
bool formatRichTextPartialStyle(EditorState editorState, String styleKey,
{Object? customValue}) {
Attributes attributes = {
styleKey: customValue ??
!_allSatisfyInSelection(editorState, styleKey, customValue ?? true),
!_allSatisfyInSelection(
editorState,
styleKey,
customValue ?? true,
),
};
return formatRichTextStyle(editorState, attributes);
}
bool _allSatisfyInSelection(
EditorState editorState, String styleKey, dynamic matchValue) {
EditorState editorState,
String styleKey,
dynamic matchValue,
) {
final selection = editorState.service.selectionService.currentSelection.value;
final nodes = editorState.service.selectionService.currentSelectedNodes;
final textNodes = nodes.whereType<TextNode>().toList(growable: false);

View File

@ -38,7 +38,7 @@ class AppFlowyEditor extends StatefulWidget {
this.customBuilders = const {},
this.shortcutEvents = const [],
this.selectionMenuItems = const [],
this.editorStyle = const EditorStyle.defaultStyle(),
required this.editorStyle,
}) : super(key: key);
final EditorState editorState;
@ -58,6 +58,8 @@ class AppFlowyEditor extends StatefulWidget {
}
class _AppFlowyEditorState extends State<AppFlowyEditor> {
Widget? services;
EditorState get editorState => widget.editorState;
@override
@ -75,19 +77,34 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
if (editorState.service != oldWidget.editorState.service) {
editorState.selectionMenuItems = widget.selectionMenuItems;
editorState.editorStyle = widget.editorStyle;
editorState.service.renderPluginService = _createRenderPlugin();
}
editorState.editorStyle = widget.editorStyle;
services = null;
}
@override
Widget build(BuildContext context) {
services ??= _buildServices(context);
return Overlay(
initialEntries: [
OverlayEntry(
builder: (context) => services!,
),
],
);
}
AppFlowyScroll _buildServices(BuildContext context) {
return AppFlowyScroll(
key: editorState.service.scrollServiceKey,
child: Padding(
padding: widget.editorStyle.padding,
child: AppFlowySelection(
key: editorState.service.selectionServiceKey,
cursorColor: widget.editorStyle.cursorColor,
selectionColor: widget.editorStyle.selectionColor,
editorState: editorState,
child: AppFlowyInput(
key: editorState.service.inputServiceKey,

View File

@ -1,8 +1,7 @@
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
// Handle delete text.
@ -42,12 +41,12 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
if (index < 0 && selection.isCollapsed) {
// 1. style
if (textNode.subtype != null) {
if (textNode.subtype == StyleKey.numberList) {
if (textNode.subtype == BuiltInAttributeKey.numberList) {
cancelNumberListPath = textNode.path;
}
transactionBuilder
..updateNode(textNode, {
StyleKey.subtype: null,
BuiltInAttributeKey.subtype: null,
textNode.subtype!: null,
})
..afterSelection = Selection.collapsed(
@ -91,7 +90,8 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
_deleteTextNodes(transactionBuilder, textNodes, selection);
transactionBuilder.commit();
if (nodeAtStart is TextNode && nodeAtStart.subtype == StyleKey.numberList) {
if (nodeAtStart is TextNode &&
nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental(
editorState,
startPosition.path,
@ -130,7 +130,7 @@ KeyEventResult _backDeleteToPreviousTextNode(
bool prevIsNumberList = false;
while (previous != null) {
if (previous is TextNode) {
if (previous.subtype == StyleKey.numberList) {
if (previous.subtype == BuiltInAttributeKey.numberList) {
prevIsNumberList = true;
}
@ -212,7 +212,8 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
_deleteTextNodes(transactionBuilder, textNodes, selection);
transactionBuilder.commit();
if (nodeAtStart is TextNode && nodeAtStart.subtype == StyleKey.numberList) {
if (nodeAtStart is TextNode &&
nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental(
editorState, startPosition.path, transactionBuilder.afterSelection!);
}
@ -236,7 +237,7 @@ KeyEventResult _mergeNextLineIntoThisLine(
transactionBuilder.deleteNode(nextNode);
transactionBuilder.commit();
if (textNode.subtype == StyleKey.numberList) {
if (textNode.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental(editorState, textNode.path, selection);
}

View File

@ -1,7 +1,7 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/html_converter.dart';
import 'package:appflowy_editor/src/document/node_iterator.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
import 'package:flutter/material.dart';
import 'package:rich_clipboard/rich_clipboard.dart';
@ -108,8 +108,8 @@ void _pasteMultipleLinesInText(
if (nodeAtPath.type == "text" && firstNode.type == "text") {
int? startNumber;
if (nodeAtPath.subtype == StyleKey.numberList) {
startNumber = nodeAtPath.attributes[StyleKey.number] as int;
if (nodeAtPath.subtype == BuiltInAttributeKey.numberList) {
startNumber = nodeAtPath.attributes[BuiltInAttributeKey.number] as int;
}
// split and merge

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import './number_list_helper.dart';
/// Handle some cases where enter is pressed and shift is not pressed.
@ -59,7 +59,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
..afterSelection = afterSelection
..commit();
if (startNode is TextNode && startNode.subtype == StyleKey.numberList) {
if (startNode is TextNode &&
startNode.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental(
editorState, selection.start.path, afterSelection);
}
@ -82,17 +83,15 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
Position(path: textNode.path, offset: 0),
);
TransactionBuilder(editorState)
..updateNode(
textNode,
Attributes.fromIterable(
StyleKey.globalStyleKeys,
value: (_) => null,
))
..updateNode(textNode, {
BuiltInAttributeKey.subtype: null,
})
..afterSelection = afterSelection
..commit();
final nextNode = textNode.next;
if (nextNode is TextNode && nextNode.subtype == StyleKey.numberList) {
if (nextNode is TextNode &&
nextNode.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental(
editorState, textNode.path, afterSelection,
beginNum: 0);
@ -103,11 +102,13 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
Position(path: textNode.path.next, offset: 0),
);
if (subtype == StyleKey.numberList) {
final prevNumber = textNode.attributes[StyleKey.number] as int;
if (subtype == BuiltInAttributeKey.numberList) {
final prevNumber =
textNode.attributes[BuiltInAttributeKey.number] as int;
final newNode = TextNode.empty();
newNode.attributes[StyleKey.subtype] = StyleKey.numberList;
newNode.attributes[StyleKey.number] = prevNumber;
newNode.attributes[BuiltInAttributeKey.subtype] =
BuiltInAttributeKey.numberList;
newNode.attributes[BuiltInAttributeKey.number] = prevNumber;
final insertPath = textNode.path;
TransactionBuilder(editorState)
..insertNode(
@ -159,7 +160,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
// If the new type of a text node is number list,
// the numbers of the following nodes should be incremental.
if (textNode.subtype == StyleKey.numberList) {
if (textNode.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental(editorState, nextPath, afterSelection);
}
@ -169,17 +170,17 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
Attributes _attributesFromPreviousLine(TextNode textNode) {
final prevAttributes = textNode.attributes;
final subType = textNode.subtype;
if (subType == null || subType == StyleKey.heading) {
if (subType == null || subType == BuiltInAttributeKey.heading) {
return {};
}
final copy = Attributes.from(prevAttributes);
if (subType == StyleKey.numberList) {
if (subType == BuiltInAttributeKey.numberList) {
return _nextNumberAttributesFromPreviousLine(copy, textNode);
}
if (subType == StyleKey.checkbox) {
copy[StyleKey.checkbox] = false;
if (subType == BuiltInAttributeKey.checkbox) {
copy[BuiltInAttributeKey.checkbox] = false;
return copy;
}
@ -188,7 +189,7 @@ Attributes _attributesFromPreviousLine(TextNode textNode) {
Attributes _nextNumberAttributesFromPreviousLine(
Attributes copy, TextNode textNode) {
final prevNum = textNode.attributes[StyleKey.number] as int?;
copy[StyleKey.number] = prevNum == null ? 1 : prevNum + 1;
final prevNum = textNode.attributes[BuiltInAttributeKey.number] as int?;
copy[BuiltInAttributeKey.number] = prevNum == null ? 1 : prevNum + 1;
return copy;
}

View File

@ -1,8 +1,8 @@
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
ShortcutEventHandler formatBoldEventHandler = (editorState, event) {
final selection = editorState.service.selectionService.currentSelection.value;
@ -55,7 +55,10 @@ ShortcutEventHandler formatHighlightEventHandler = (editorState, event) {
if (selection == null || textNodes.isEmpty) {
return KeyEventResult.ignored;
}
formatHighlight(editorState);
formatHighlight(
editorState,
editorState.editorStyle.textStyle.highlightColorHex,
);
return KeyEventResult.handled;
};
@ -73,3 +76,14 @@ ShortcutEventHandler formatLinkEventHandler = (editorState, event) {
}
return KeyEventResult.ignored;
};
ShortcutEventHandler formatEmbedCodeEventHandler = (editorState, event) {
final selection = editorState.service.selectionService.currentSelection.value;
final nodes = editorState.service.selectionService.currentSelectedNodes;
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
if (selection == null || textNodes.isEmpty) {
return KeyEventResult.ignored;
}
formatEmbedCode(editorState);
return KeyEventResult.ignored;
};

View File

@ -1,6 +1,6 @@
import 'package:appflowy_editor/src/document/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/document/attributes.dart';
@ -11,7 +11,7 @@ void makeFollowingNodesIncremental(
if (insertNode == null) {
return;
}
beginNum ??= insertNode.attributes[StyleKey.number] as int;
beginNum ??= insertNode.attributes[BuiltInAttributeKey.number] as int;
int numPtr = beginNum + 1;
var ptr = insertNode.next;
@ -19,13 +19,13 @@ void makeFollowingNodesIncremental(
final builder = TransactionBuilder(editorState);
while (ptr != null) {
if (ptr.subtype != StyleKey.numberList) {
if (ptr.subtype != BuiltInAttributeKey.numberList) {
break;
}
final currentNum = ptr.attributes[StyleKey.number] as int;
final currentNum = ptr.attributes[BuiltInAttributeKey.number] as int;
if (currentNum != numPtr) {
Attributes updateAttributes = {};
updateAttributes[StyleKey.number] = numPtr;
updateAttributes[BuiltInAttributeKey.number] = numPtr;
builder.updateNode(ptr, updateAttributes);
}

View File

@ -1,14 +1,14 @@
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import './number_list_helper.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
@visibleForTesting
List<String> get checkboxListSymbols => _checkboxListSymbols;
@ -68,7 +68,7 @@ ShortcutEventHandler whiteSpaceHandler = (editorState, event) {
KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
String matchText, String numText) {
if (textNode.subtype == StyleKey.bulletedList) {
if (textNode.subtype == BuiltInAttributeKey.bulletedList) {
return KeyEventResult.ignored;
}
@ -86,8 +86,9 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
final prevNode = textNode.previous;
if (prevNode != null &&
prevNode is TextNode &&
prevNode.attributes[StyleKey.subtype] == StyleKey.numberList) {
final prevNumber = prevNode.attributes[StyleKey.number] as int;
prevNode.attributes[BuiltInAttributeKey.subtype] ==
BuiltInAttributeKey.numberList) {
final prevNumber = prevNode.attributes[BuiltInAttributeKey.number] as int;
if (numValue != prevNumber + 1) {
return KeyEventResult.ignored;
}
@ -102,8 +103,10 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
TransactionBuilder(editorState)
..deleteText(textNode, 0, matchText.length)
..updateNode(textNode,
{StyleKey.subtype: StyleKey.numberList, StyleKey.number: numValue})
..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
BuiltInAttributeKey.number: numValue
})
..afterSelection = afterSelection
..commit();
@ -113,13 +116,13 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
}
KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
if (textNode.subtype == StyleKey.bulletedList) {
if (textNode.subtype == BuiltInAttributeKey.bulletedList) {
return KeyEventResult.ignored;
}
TransactionBuilder(editorState)
..deleteText(textNode, 0, 1)
..updateNode(textNode, {
StyleKey.subtype: StyleKey.bulletedList,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
})
..afterSelection = Selection.collapsed(
Position(
@ -132,7 +135,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
}
KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
if (textNode.subtype == StyleKey.checkbox) {
if (textNode.subtype == BuiltInAttributeKey.checkbox) {
return KeyEventResult.ignored;
}
final String symbol;
@ -152,8 +155,8 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
TransactionBuilder(editorState)
..deleteText(textNode, 0, symbol.length)
..updateNode(textNode, {
StyleKey.subtype: StyleKey.checkbox,
StyleKey.checkbox: check,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.checkbox: check,
})
..afterSelection = Selection.collapsed(
Position(
@ -178,8 +181,8 @@ KeyEventResult _toHeadingStyle(
TransactionBuilder(editorState)
..deleteText(textNode, 0, x)
..updateNode(textNode, {
StyleKey.subtype: StyleKey.heading,
StyleKey.heading: hX,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: hX,
})
..afterSelection = Selection.collapsed(
Position(

View File

@ -82,7 +82,7 @@ abstract class AppFlowySelectionService {
class AppFlowySelection extends StatefulWidget {
const AppFlowySelection({
Key? key,
this.cursorColor = Colors.black,
this.cursorColor = const Color(0xFF00BCF0),
this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
required this.editorState,
required this.child,
@ -343,8 +343,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
currentSelectedNodes = nodes;
// TODO: need to be refactored.
Rect? topmostRect;
Offset? toolbarOffset;
LayerLink? layerLink;
final editorOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final backwardNodes =
selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
@ -381,13 +383,20 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
}
}
const baseToolbarOffset = Offset(0, 35.0);
final rects = selectable.getRectsInSelection(newSelection);
for (final rect in rects) {
// TODO: Need to compute more precise location.
topmostRect ??= rect;
layerLink ??= node.layerLink;
final selectionRect = _transformRectToGlobal(selectable, rect);
selectionRects.add(selectionRect);
selectionRects.add(_transformRectToGlobal(selectable, rect));
// TODO: Need to compute more precise location.
if ((selectionRect.topLeft.dy - editorOffset.dy) <=
baseToolbarOffset.dy) {
toolbarOffset ??= rect.bottomLeft;
} else {
toolbarOffset ??= rect.topLeft - baseToolbarOffset;
}
layerLink ??= node.layerLink;
final overlay = OverlayEntry(
builder: (context) => SelectionWidget(
@ -402,9 +411,11 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
Overlay.of(context)?.insertAll(_selectionAreas);
if (topmostRect != null && layerLink != null) {
editorState.service.toolbarService
?.showInOffset(topmostRect.topLeft, layerLink);
if (toolbarOffset != null && layerLink != null) {
editorState.service.toolbarService?.showInOffset(
toolbarOffset,
layerLink,
);
}
}

View File

@ -144,6 +144,12 @@ List<ShortcutEvent> builtInShortcutEvents = [
windowsCommand: 'ctrl+shift+h',
handler: formatHighlightEventHandler,
),
ShortcutEvent(
key: 'Format embed code',
command: 'meta+e',
windowsCommand: 'ctrl+e',
handler: formatEmbedCodeEventHandler,
),
ShortcutEvent(
key: 'Format link',
command: 'meta+k',

View File

@ -44,7 +44,7 @@ class _FlowyToolbarState extends State<FlowyToolbar>
key: _toolbarWidgetKey,
editorState: widget.editorState,
layerLink: layerLink,
offset: offset.translate(0, -37.0),
offset: offset,
items: _filterItems(defaultToolbarItems),
),
);

View File

@ -22,6 +22,7 @@ dependencies:
provider: ^6.0.3
url_launcher: ^6.1.5
logging: ^1.0.2
intl_utils: ^2.7.0
dev_dependencies:
flutter_test:
@ -66,3 +67,13 @@ flutter:
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages
flutter_intl:
enabled: true
class_name: AppFlowyEditorLocalizations
main_locale: en
arb_dir: lib/l10n
output_dir: lib/src/l10n
use_deferred_loading: false
localizely:
project_id: b7199c7d-eca0-4025-894d-230cdcafa9aa

View File

@ -23,18 +23,20 @@ class EditorWidgetTester {
_editorState.service.selectionService.currentSelection.value;
Future<EditorWidgetTester> startTesting() async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: AppFlowyEditor(
editorState: _editorState,
editorStyle: const EditorStyle(
padding: EdgeInsets.symmetric(vertical: 30),
),
),
final app = MaterialApp(
localizationsDelegates: const [
AppFlowyEditorLocalizations.delegate,
],
supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales,
home: Scaffold(
body: AppFlowyEditor(
editorState: _editorState,
editorStyle: EditorStyle.defaultStyle(),
),
),
);
await tester.pumpWidget(app);
await tester.pump();
return this;
}

View File

@ -118,6 +118,9 @@ extension on LogicalKeyboardKey {
if (this == LogicalKeyboardKey.keyC) {
return PhysicalKeyboardKey.keyC;
}
if (this == LogicalKeyboardKey.keyE) {
return PhysicalKeyboardKey.keyE;
}
if (this == LogicalKeyboardKey.keyI) {
return PhysicalKeyboardKey.keyI;
}

View File

@ -1,9 +1,10 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
void main() async {
setUpAll(() {
@ -25,15 +26,15 @@ void main() async {
..insertTextNode(
'',
attributes: {
StyleKey.subtype: StyleKey.checkbox,
StyleKey.checkbox: false,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.checkbox: false,
},
delta: Delta([
TextInsert(text, {
StyleKey.bold: true,
StyleKey.italic: true,
StyleKey.underline: true,
StyleKey.strikethrough: true,
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.italic: true,
BuiltInAttributeKey.underline: true,
BuiltInAttributeKey.strikethrough: true,
}),
]),
);

View File

@ -1,10 +1,11 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
void main() async {
setUpAll(() {
@ -229,7 +230,7 @@ void main() async {
expect(
node.allSatisfyInSelection(
code,
StyleKey.code,
BuiltInAttributeKey.code,
(value) {
return value == true;
},
@ -319,7 +320,7 @@ void main() async {
expect(
node.allSatisfyInSelection(
selection,
StyleKey.backgroundColor,
BuiltInAttributeKey.backgroundColor,
(value) {
return value == blue;
},

View File

@ -1,5 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
@ -13,91 +13,99 @@ void main() async {
});
group('selection_menu_widget.dart', () {
for (var i = 0; i < defaultSelectionMenuItems.length; i++) {
testWidgets('Selects number.$i item in selection menu', (tester) async {
final editor = await _prepare(tester);
for (var j = 0; j < i; j++) {
await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
}
// const i = defaultSelectionMenuItems.length;
//
// Because the `defaultSelectionMenuItems` uses localization,
// and the MaterialApp has not been initialized at the time of getting the value,
// it will crash.
//
// Use const value temporarily instead.
const i = 7;
testWidgets('Selects number.$i item in selection menu', (tester) async {
final editor = await _prepare(tester);
for (var j = 0; j < i; j++) {
await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
}
await editor.pressLogicKey(LogicalKeyboardKey.enter);
expect(
find.byType(SelectionMenuWidget, skipOffstage: false),
findsNothing,
);
if (defaultSelectionMenuItems[i].name != 'Image') {
await _testDefaultSelectionMenuItems(i, editor);
}
});
}
});
await editor.pressLogicKey(LogicalKeyboardKey.enter);
expect(
find.byType(SelectionMenuWidget, skipOffstage: false),
findsNothing,
);
if (defaultSelectionMenuItems[i].name != 'Image') {
await _testDefaultSelectionMenuItems(i, editor);
}
});
testWidgets('Search item in selection menu util no results', (tester) async {
final editor = await _prepare(tester);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(3),
);
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(4),
);
await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(3),
);
await editor.pressLogicKey(LogicalKeyboardKey.keyX);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(1),
);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(1),
);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNothing,
);
});
testWidgets('Search item in selection menu util no results',
(tester) async {
final editor = await _prepare(tester);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(3),
);
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(4),
);
await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(3),
);
await editor.pressLogicKey(LogicalKeyboardKey.keyX);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(1),
);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(1),
);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNothing,
);
});
testWidgets('Search item in selection menu and presses esc', (tester) async {
final editor = await _prepare(tester);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(3),
);
await editor.pressLogicKey(LogicalKeyboardKey.escape);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNothing,
);
});
testWidgets('Search item in selection menu and presses esc',
(tester) async {
final editor = await _prepare(tester);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(3),
);
await editor.pressLogicKey(LogicalKeyboardKey.escape);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNothing,
);
});
testWidgets('Search item in selection menu and presses backspace',
(tester) async {
final editor = await _prepare(tester);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(3),
);
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNothing,
);
testWidgets('Search item in selection menu and presses backspace',
(tester) async {
final editor = await _prepare(tester);
await editor.pressLogicKey(LogicalKeyboardKey.keyT);
await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(3),
);
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNothing,
);
});
});
}
@ -135,18 +143,18 @@ Future<void> _testDefaultSelectionMenuItems(
if (item.name == 'Text') {
expect(node?.subtype == null, true);
} else if (item.name == 'Heading 1') {
expect(node?.subtype, StyleKey.heading);
expect(node?.attributes.heading, StyleKey.h1);
expect(node?.subtype, BuiltInAttributeKey.heading);
expect(node?.attributes.heading, BuiltInAttributeKey.h1);
} else if (item.name == 'Heading 2') {
expect(node?.subtype, StyleKey.heading);
expect(node?.attributes.heading, StyleKey.h2);
expect(node?.subtype, BuiltInAttributeKey.heading);
expect(node?.attributes.heading, BuiltInAttributeKey.h2);
} else if (item.name == 'Heading 3') {
expect(node?.subtype, StyleKey.heading);
expect(node?.attributes.heading, StyleKey.h3);
expect(node?.subtype, BuiltInAttributeKey.heading);
expect(node?.attributes.heading, BuiltInAttributeKey.h3);
} else if (item.name == 'Bulleted list') {
expect(node?.subtype, StyleKey.bulletedList);
expect(node?.subtype, BuiltInAttributeKey.bulletedList);
} else if (item.name == 'Checkbox') {
expect(node?.subtype, StyleKey.checkbox);
expect(node?.subtype, BuiltInAttributeKey.checkbox);
expect(node?.attributes.check, false);
}
}

View File

@ -1,10 +1,11 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:network_image_mock/network_image_mock.dart';
import '../../infra/test_editor.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
void main() async {
setUpAll(() {
@ -132,17 +133,18 @@ void main() async {
//
testWidgets('Presses backspace key in styled text (checkbox)',
(tester) async {
await _deleteStyledTextByBackspace(tester, StyleKey.checkbox);
await _deleteStyledTextByBackspace(tester, BuiltInAttributeKey.checkbox);
});
testWidgets('Presses backspace key in styled text (bulletedList)',
(tester) async {
await _deleteStyledTextByBackspace(tester, StyleKey.bulletedList);
await _deleteStyledTextByBackspace(
tester, BuiltInAttributeKey.bulletedList);
});
testWidgets('Presses backspace key in styled text (heading)', (tester) async {
await _deleteStyledTextByBackspace(tester, StyleKey.heading);
await _deleteStyledTextByBackspace(tester, BuiltInAttributeKey.heading);
});
testWidgets('Presses backspace key in styled text (quote)', (tester) async {
await _deleteStyledTextByBackspace(tester, StyleKey.quote);
await _deleteStyledTextByBackspace(tester, BuiltInAttributeKey.quote);
});
// Before
@ -157,17 +159,17 @@ void main() async {
// [Style] Welcome to Appflowy 😁
//
testWidgets('Presses delete key in styled text (checkbox)', (tester) async {
await _deleteStyledTextByDelete(tester, StyleKey.checkbox);
await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.checkbox);
});
testWidgets('Presses delete key in styled text (bulletedList)',
(tester) async {
await _deleteStyledTextByDelete(tester, StyleKey.bulletedList);
await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.bulletedList);
});
testWidgets('Presses delete key in styled text (heading)', (tester) async {
await _deleteStyledTextByDelete(tester, StyleKey.heading);
await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.heading);
});
testWidgets('Presses delete key in styled text (quote)', (tester) async {
await _deleteStyledTextByDelete(tester, StyleKey.quote);
await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.quote);
});
// Before
@ -250,7 +252,7 @@ void main() async {
await editor.pressLogicKey(LogicalKeyboardKey.space);
expect(
(editor.nodeAtPath([0]) as TextNode).attributes.heading,
StyleKey.h1,
BuiltInAttributeKey.h1,
);
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
@ -263,7 +265,7 @@ void main() async {
await editor.pressLogicKey(LogicalKeyboardKey.space);
expect(
(editor.nodeAtPath([0]) as TextNode).attributes.heading,
StyleKey.h1,
BuiltInAttributeKey.h1,
);
});
}
@ -330,14 +332,14 @@ Future<void> _deleteStyledTextByBackspace(
WidgetTester tester, String style) async {
const text = 'Welcome to Appflowy 😁';
Attributes attributes = {
StyleKey.subtype: style,
BuiltInAttributeKey.subtype: style,
};
if (style == StyleKey.checkbox) {
attributes[StyleKey.checkbox] = true;
} else if (style == StyleKey.numberList) {
attributes[StyleKey.number] = 1;
} else if (style == StyleKey.heading) {
attributes[StyleKey.heading] = StyleKey.h1;
if (style == BuiltInAttributeKey.checkbox) {
attributes[BuiltInAttributeKey.checkbox] = true;
} else if (style == BuiltInAttributeKey.numberList) {
attributes[BuiltInAttributeKey.number] = 1;
} else if (style == BuiltInAttributeKey.heading) {
attributes[BuiltInAttributeKey.heading] = BuiltInAttributeKey.h1;
}
final editor = tester.editor
..insertTextNode(text)
@ -377,14 +379,14 @@ Future<void> _deleteStyledTextByDelete(
WidgetTester tester, String style) async {
const text = 'Welcome to Appflowy 😁';
Attributes attributes = {
StyleKey.subtype: style,
BuiltInAttributeKey.subtype: style,
};
if (style == StyleKey.checkbox) {
attributes[StyleKey.checkbox] = true;
} else if (style == StyleKey.numberList) {
attributes[StyleKey.number] = 1;
} else if (style == StyleKey.heading) {
attributes[StyleKey.heading] = StyleKey.h1;
if (style == BuiltInAttributeKey.checkbox) {
attributes[BuiltInAttributeKey.checkbox] = true;
} else if (style == BuiltInAttributeKey.numberList) {
attributes[BuiltInAttributeKey.number] = 1;
} else if (style == BuiltInAttributeKey.heading) {
attributes[BuiltInAttributeKey.heading] = BuiltInAttributeKey.h1;
}
final editor = tester.editor
..insertTextNode(text)

View File

@ -1,8 +1,8 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
void main() async {
setUpAll(() {
@ -95,16 +95,16 @@ void main() async {
// [Style] Welcome to Appflowy 😁
// [Style]
testWidgets('Presses enter key in bulleted list', (tester) async {
await _testStyleNeedToBeCopy(tester, StyleKey.bulletedList);
await _testStyleNeedToBeCopy(tester, BuiltInAttributeKey.bulletedList);
});
testWidgets('Presses enter key in numbered list', (tester) async {
await _testStyleNeedToBeCopy(tester, StyleKey.numberList);
await _testStyleNeedToBeCopy(tester, BuiltInAttributeKey.numberList);
});
testWidgets('Presses enter key in checkbox styled text', (tester) async {
await _testStyleNeedToBeCopy(tester, StyleKey.checkbox);
await _testStyleNeedToBeCopy(tester, BuiltInAttributeKey.checkbox);
});
testWidgets('Presses enter key in quoted text', (tester) async {
await _testStyleNeedToBeCopy(tester, StyleKey.quote);
await _testStyleNeedToBeCopy(tester, BuiltInAttributeKey.quote);
});
testWidgets('Presses enter key in multiple selection from top to bottom',
@ -143,12 +143,12 @@ void main() async {
Future<void> _testStyleNeedToBeCopy(WidgetTester tester, String style) async {
const text = 'Welcome to Appflowy 😁';
Attributes attributes = {
StyleKey.subtype: style,
BuiltInAttributeKey.subtype: style,
};
if (style == StyleKey.checkbox) {
attributes[StyleKey.checkbox] = true;
} else if (style == StyleKey.numberList) {
attributes[StyleKey.number] = 1;
if (style == BuiltInAttributeKey.checkbox) {
attributes[BuiltInAttributeKey.checkbox] = true;
} else if (style == BuiltInAttributeKey.numberList) {
attributes[BuiltInAttributeKey.number] = 1;
}
final editor = tester.editor
..insertTextNode(text)

View File

@ -2,24 +2,24 @@ import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('update_text_style_by_command_x_handler.dart', () {
group('format_style_handler.dart', () {
testWidgets('Presses Command + B to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
StyleKey.bold,
BuiltInAttributeKey.bold,
true,
LogicalKeyboardKey.keyB,
);
@ -27,7 +27,7 @@ void main() async {
testWidgets('Presses Command + I to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
StyleKey.italic,
BuiltInAttributeKey.italic,
true,
LogicalKeyboardKey.keyI,
);
@ -35,7 +35,7 @@ void main() async {
testWidgets('Presses Command + U to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
StyleKey.underline,
BuiltInAttributeKey.underline,
true,
LogicalKeyboardKey.keyU,
);
@ -44,7 +44,7 @@ void main() async {
(tester) async {
await _testUpdateTextStyleByCommandX(
tester,
StyleKey.strikethrough,
BuiltInAttributeKey.strikethrough,
true,
LogicalKeyboardKey.keyS,
);
@ -52,10 +52,11 @@ void main() async {
testWidgets('Presses Command + Shift + H to update text style',
(tester) async {
// FIXME: customize the highlight color instead of using magic number.
await _testUpdateTextStyleByCommandX(
tester,
StyleKey.backgroundColor,
defaultHighlightColor,
BuiltInAttributeKey.backgroundColor,
'0x6000BCF0',
LogicalKeyboardKey.keyH,
);
});
@ -63,6 +64,15 @@ void main() async {
testWidgets('Presses Command + K to trigger link menu', (tester) async {
await _testLinkMenuInSingleTextSelection(tester);
});
testWidgets('Presses Command + E to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
BuiltInAttributeKey.code,
true,
LogicalKeyboardKey.keyE,
);
});
});
}
@ -256,7 +266,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
expect(
node.allSatisfyInSelection(
selection,
StyleKey.href,
BuiltInAttributeKey.href,
(value) => value == link,
),
true);
@ -293,7 +303,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
expect(
node.allSatisfyInSelection(
selection,
StyleKey.href,
BuiltInAttributeKey.href,
(value) => value == link,
),
false);

View File

@ -1,9 +1,10 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
void main() async {
setUpAll(() {
@ -45,8 +46,8 @@ void main() async {
final textNode = (editor.nodeAtPath([i - 1]) as TextNode);
expect(textNode.subtype, StyleKey.heading);
// StyleKey.h1 ~ StyleKey.h6
expect(textNode.subtype, BuiltInAttributeKey.heading);
// BuiltInAttributeKey.h1 ~ BuiltInAttributeKey.h6
expect(textNode.attributes.heading, 'h$i');
}
});
@ -85,8 +86,8 @@ void main() async {
final textNode = (editor.nodeAtPath([i - 1]) as TextNode);
expect(textNode.subtype, StyleKey.heading);
// StyleKey.h1 ~ StyleKey.h6
expect(textNode.subtype, BuiltInAttributeKey.heading);
// BuiltInAttributeKey.h1 ~ BuiltInAttributeKey.h6
expect(textNode.attributes.heading, 'h$i');
expect(textNode.toRawString().startsWith('##'), true);
}
@ -117,8 +118,8 @@ void main() async {
await editor.insertText(textNode, '#' * i, 0);
await editor.pressLogicKey(LogicalKeyboardKey.space);
expect(textNode.subtype, StyleKey.heading);
// StyleKey.h2 ~ StyleKey.h6
expect(textNode.subtype, BuiltInAttributeKey.heading);
// BuiltInAttributeKey.h2 ~ BuiltInAttributeKey.h6
expect(textNode.attributes.heading, 'h$i');
}
});
@ -136,7 +137,7 @@ void main() async {
);
await editor.insertText(textNode, symbol, 0);
await editor.pressLogicKey(LogicalKeyboardKey.space);
expect(textNode.subtype, StyleKey.checkbox);
expect(textNode.subtype, BuiltInAttributeKey.checkbox);
expect(textNode.attributes.check, false);
}
});
@ -154,7 +155,7 @@ void main() async {
);
await editor.insertText(textNode, symbol, 0);
await editor.pressLogicKey(LogicalKeyboardKey.space);
expect(textNode.subtype, StyleKey.checkbox);
expect(textNode.subtype, BuiltInAttributeKey.checkbox);
expect(textNode.attributes.check, true);
}
});
@ -171,7 +172,7 @@ void main() async {
);
await editor.insertText(textNode, symbol, 0);
await editor.pressLogicKey(LogicalKeyboardKey.space);
expect(textNode.subtype, StyleKey.bulletedList);
expect(textNode.subtype, BuiltInAttributeKey.bulletedList);
}
});
});

View File

@ -81,6 +81,8 @@ void main() async {
editor.documentSelection,
Selection.single(path: [1], startOffset: 0),
);
tester.pumpAndSettle();
});
});
}

View File

@ -1,10 +1,10 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import '../infra/test_editor.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
void main() async {
setUpAll(() {
@ -45,13 +45,13 @@ void main() async {
});
testWidgets(
'Test toolbar service in single text selection with StyleKey.partialStyleKeys',
'Test toolbar service in single text selection with BuiltInAttributeKey.partialStyleKeys',
(tester) async {
final attributes = StyleKey.partialStyleKeys.fold<Attributes>({},
(previousValue, element) {
if (element == StyleKey.backgroundColor) {
final attributes = BuiltInAttributeKey.partialStyleKeys
.fold<Attributes>({}, (previousValue, element) {
if (element == BuiltInAttributeKey.backgroundColor) {
previousValue[element] = '0x6000BCF0';
} else if (element == StyleKey.href) {
} else if (element == BuiltInAttributeKey.href) {
previousValue[element] = 'appflowy.io';
} else {
previousValue[element] = true;
@ -77,11 +77,11 @@ void main() async {
expect(find.byType(ToolbarWidget), findsOneWidget);
void testHighlight(bool expectedValue) {
for (final styleKey in StyleKey.partialStyleKeys) {
for (final styleKey in BuiltInAttributeKey.partialStyleKeys) {
var key = styleKey;
if (styleKey == StyleKey.backgroundColor) {
if (styleKey == BuiltInAttributeKey.backgroundColor) {
key = 'highlight';
} else if (styleKey == StyleKey.href) {
} else if (styleKey == BuiltInAttributeKey.href) {
key = 'link';
} else {
continue;
@ -116,22 +116,24 @@ void main() async {
});
testWidgets(
'Test toolbar service in single text selection with StyleKey.globalStyleKeys',
'Test toolbar service in single text selection with BuiltInAttributeKey.globalStyleKeys',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..insertTextNode(text, attributes: {
StyleKey.subtype: StyleKey.heading,
StyleKey.heading: StyleKey.h1,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
})
..insertTextNode(
text,
attributes: {StyleKey.subtype: StyleKey.quote},
attributes: {BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote},
)
..insertTextNode(
text,
attributes: {StyleKey.subtype: StyleKey.bulletedList},
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
},
);
await editor.startTesting();
@ -167,12 +169,12 @@ void main() async {
..insertTextNode(
null,
attributes: {
StyleKey.subtype: StyleKey.heading,
StyleKey.heading: StyleKey.h1,
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
},
delta: Delta([
TextInsert(text, {
StyleKey.bold: true,
BuiltInAttributeKey.bold: true,
})
]),
)