mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-25 07:07:32 -04:00
Added : customize the color and background color of selected text (#1601)
* Added Emoji Support * Added Color Picker for font color and background color * chore: revert code * feat: re-implement the color picker * test: add test case for adding color * test: update appflowy_editor test flag Co-authored-by: Muhammad Rizwan <haris.arshad.2010@gmail.com> Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
parent
f9cc05319b
commit
e4b07e69fa
15 changed files with 788 additions and 6 deletions
2
.github/workflows/appflowy_editor_test.yml
vendored
2
.github/workflows/appflowy_editor_test.yml
vendored
|
@ -44,7 +44,7 @@ jobs:
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
name: appflowy_editor
|
name: appflowy_editor
|
||||||
flags: appflowy editor
|
flags: appflowy_editor
|
||||||
env_vars: ${{ matrix.os }}
|
env_vars: ${{ matrix.os }}
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
|
@ -96,7 +96,8 @@
|
||||||
"inlineCode": "Inline Code",
|
"inlineCode": "Inline Code",
|
||||||
"quote": "Quote Block",
|
"quote": "Quote Block",
|
||||||
"header": "Header",
|
"header": "Header",
|
||||||
"highlight": "Highlight"
|
"highlight": "Highlight",
|
||||||
|
"color": "Color"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"lightMode": "Switch to Light mode",
|
"lightMode": "Switch to Light mode",
|
||||||
|
|
|
@ -90,7 +90,8 @@
|
||||||
"inlineCode": "Código embebido",
|
"inlineCode": "Código embebido",
|
||||||
"quote": "Cita",
|
"quote": "Cita",
|
||||||
"header": "Título",
|
"header": "Título",
|
||||||
"highlight": "Resaltado"
|
"highlight": "Resaltado",
|
||||||
|
"color": "Color"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"lightMode": "Cambiar a modo Claro",
|
"lightMode": "Cambiar a modo Claro",
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 5.2L2.84615 7L9 1" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 198 B |
|
@ -16,6 +16,8 @@
|
||||||
"@heading3": {},
|
"@heading3": {},
|
||||||
"highlight": "Highlight",
|
"highlight": "Highlight",
|
||||||
"@highlight": {},
|
"@highlight": {},
|
||||||
|
"color": "Color",
|
||||||
|
"@color": {},
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"@image": {},
|
"@image": {},
|
||||||
"italic": "Italic",
|
"italic": "Italic",
|
||||||
|
@ -31,5 +33,45 @@
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"@text": {},
|
"@text": {},
|
||||||
"underline": "Underline",
|
"underline": "Underline",
|
||||||
"@underline": {}
|
"@underline": {},
|
||||||
|
"fontColorDefault": "Default",
|
||||||
|
"@fontColorDefault": {},
|
||||||
|
"fontColorGray": "Gray",
|
||||||
|
"@fontColorGray": {},
|
||||||
|
"fontColorBrown": "Brown",
|
||||||
|
"@fontColorBrown": {},
|
||||||
|
"fontColorOrange": "Orange",
|
||||||
|
"@fontColorOrange": {},
|
||||||
|
"fontColorYellow": "Yellow",
|
||||||
|
"@fontColorYellow": {},
|
||||||
|
"fontColorGreen": "Green",
|
||||||
|
"@fontColorGreen": {},
|
||||||
|
"fontColorBlue": "Blue",
|
||||||
|
"@fontColorBlue": {},
|
||||||
|
"fontColorPurple": "Purple",
|
||||||
|
"@fontColorPurple": {},
|
||||||
|
"fontColorPink": "Pink",
|
||||||
|
"@fontColorPink": {},
|
||||||
|
"fontColorRed": "Red",
|
||||||
|
"@fontColorRed": {},
|
||||||
|
"backgroundColorDefault": "Default background",
|
||||||
|
"@backgroundColorDefault": {},
|
||||||
|
"backgroundColorGray": "Gray background",
|
||||||
|
"@backgroundColorGray": {},
|
||||||
|
"backgroundColorBrown": "Brown background",
|
||||||
|
"@backgroundColorBrown": {},
|
||||||
|
"backgroundColorOrange": "Orange background",
|
||||||
|
"@backgroundColorOrange": {},
|
||||||
|
"backgroundColorYellow": "Yellow background",
|
||||||
|
"@backgroundColorYellow": {},
|
||||||
|
"backgroundColorGreen": "Green background",
|
||||||
|
"@backgroundColorGreen": {},
|
||||||
|
"backgroundColorBlue": "Blue background",
|
||||||
|
"@backgroundColorBlue": {},
|
||||||
|
"backgroundColorPurple": "Purple background",
|
||||||
|
"@backgroundColorPurple": {},
|
||||||
|
"backgroundColorPink": "Pink background",
|
||||||
|
"@backgroundColorPink": {},
|
||||||
|
"backgroundColorRed": "Red background",
|
||||||
|
"@backgroundColorRed": {}
|
||||||
}
|
}
|
|
@ -45,6 +45,7 @@ class BuiltInAttributeKey {
|
||||||
BuiltInAttributeKey.underline,
|
BuiltInAttributeKey.underline,
|
||||||
BuiltInAttributeKey.strikethrough,
|
BuiltInAttributeKey.strikethrough,
|
||||||
BuiltInAttributeKey.backgroundColor,
|
BuiltInAttributeKey.backgroundColor,
|
||||||
|
BuiltInAttributeKey.color,
|
||||||
BuiltInAttributeKey.href,
|
BuiltInAttributeKey.href,
|
||||||
BuiltInAttributeKey.code,
|
BuiltInAttributeKey.code,
|
||||||
];
|
];
|
||||||
|
|
|
@ -34,6 +34,17 @@ extension TextNodeExtension on TextNode {
|
||||||
return value != null;
|
return value != null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool allSatisfyFontColorInSelection(Selection selection) =>
|
||||||
|
allSatisfyInSelection(selection, BuiltInAttributeKey.color, (value) {
|
||||||
|
return value != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
bool allSatisfyBackgroundColorInSelection(Selection selection) =>
|
||||||
|
allSatisfyInSelection(selection, BuiltInAttributeKey.backgroundColor,
|
||||||
|
(value) {
|
||||||
|
return value != null;
|
||||||
|
});
|
||||||
|
|
||||||
bool allSatisfyBoldInSelection(Selection selection) =>
|
bool allSatisfyBoldInSelection(Selection selection) =>
|
||||||
allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) {
|
allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) {
|
||||||
return value == true;
|
return value == true;
|
||||||
|
|
|
@ -22,10 +22,41 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
|
|
||||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||||
|
"backgroundColorBlue":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Blue background"),
|
||||||
|
"backgroundColorBrown":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Brown background"),
|
||||||
|
"backgroundColorDefault":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Default background"),
|
||||||
|
"backgroundColorGray":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Gray background"),
|
||||||
|
"backgroundColorGreen":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Green background"),
|
||||||
|
"backgroundColorOrange":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Orange background"),
|
||||||
|
"backgroundColorPink":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Pink background"),
|
||||||
|
"backgroundColorPurple":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Purple background"),
|
||||||
|
"backgroundColorRed":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Red background"),
|
||||||
|
"backgroundColorYellow":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Yellow background"),
|
||||||
"bold": MessageLookupByLibrary.simpleMessage("Bold"),
|
"bold": MessageLookupByLibrary.simpleMessage("Bold"),
|
||||||
"bulletedList": MessageLookupByLibrary.simpleMessage("Bulleted List"),
|
"bulletedList": MessageLookupByLibrary.simpleMessage("Bulleted List"),
|
||||||
"checkbox": MessageLookupByLibrary.simpleMessage("Checkbox"),
|
"checkbox": MessageLookupByLibrary.simpleMessage("Checkbox"),
|
||||||
|
"color": MessageLookupByLibrary.simpleMessage("Color"),
|
||||||
"embedCode": MessageLookupByLibrary.simpleMessage("Embed Code"),
|
"embedCode": MessageLookupByLibrary.simpleMessage("Embed Code"),
|
||||||
|
"fontColorBlue": MessageLookupByLibrary.simpleMessage("Blue"),
|
||||||
|
"fontColorBrown": MessageLookupByLibrary.simpleMessage("Brown"),
|
||||||
|
"fontColorDefault": MessageLookupByLibrary.simpleMessage("Default"),
|
||||||
|
"fontColorGray": MessageLookupByLibrary.simpleMessage("Gray"),
|
||||||
|
"fontColorGreen": MessageLookupByLibrary.simpleMessage("Green"),
|
||||||
|
"fontColorOrange": MessageLookupByLibrary.simpleMessage("Orange"),
|
||||||
|
"fontColorPink": MessageLookupByLibrary.simpleMessage("Pink"),
|
||||||
|
"fontColorPurple": MessageLookupByLibrary.simpleMessage("Purple"),
|
||||||
|
"fontColorRed": MessageLookupByLibrary.simpleMessage("Red"),
|
||||||
|
"fontColorYellow": MessageLookupByLibrary.simpleMessage("Yellow"),
|
||||||
"heading1": MessageLookupByLibrary.simpleMessage("H1"),
|
"heading1": MessageLookupByLibrary.simpleMessage("H1"),
|
||||||
"heading2": MessageLookupByLibrary.simpleMessage("H2"),
|
"heading2": MessageLookupByLibrary.simpleMessage("H2"),
|
||||||
"heading3": MessageLookupByLibrary.simpleMessage("H3"),
|
"heading3": MessageLookupByLibrary.simpleMessage("H3"),
|
||||||
|
|
|
@ -131,6 +131,16 @@ class AppFlowyEditorLocalizations {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Color`
|
||||||
|
String get color {
|
||||||
|
return Intl.message(
|
||||||
|
'Color',
|
||||||
|
name: 'color',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `Image`
|
/// `Image`
|
||||||
String get image {
|
String get image {
|
||||||
return Intl.message(
|
return Intl.message(
|
||||||
|
@ -210,6 +220,206 @@ class AppFlowyEditorLocalizations {
|
||||||
args: [],
|
args: [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Default`
|
||||||
|
String get fontColorDefault {
|
||||||
|
return Intl.message(
|
||||||
|
'Default',
|
||||||
|
name: 'fontColorDefault',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Gray`
|
||||||
|
String get fontColorGray {
|
||||||
|
return Intl.message(
|
||||||
|
'Gray',
|
||||||
|
name: 'fontColorGray',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Brown`
|
||||||
|
String get fontColorBrown {
|
||||||
|
return Intl.message(
|
||||||
|
'Brown',
|
||||||
|
name: 'fontColorBrown',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Orange`
|
||||||
|
String get fontColorOrange {
|
||||||
|
return Intl.message(
|
||||||
|
'Orange',
|
||||||
|
name: 'fontColorOrange',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Yellow`
|
||||||
|
String get fontColorYellow {
|
||||||
|
return Intl.message(
|
||||||
|
'Yellow',
|
||||||
|
name: 'fontColorYellow',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Green`
|
||||||
|
String get fontColorGreen {
|
||||||
|
return Intl.message(
|
||||||
|
'Green',
|
||||||
|
name: 'fontColorGreen',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Blue`
|
||||||
|
String get fontColorBlue {
|
||||||
|
return Intl.message(
|
||||||
|
'Blue',
|
||||||
|
name: 'fontColorBlue',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Purple`
|
||||||
|
String get fontColorPurple {
|
||||||
|
return Intl.message(
|
||||||
|
'Purple',
|
||||||
|
name: 'fontColorPurple',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Pink`
|
||||||
|
String get fontColorPink {
|
||||||
|
return Intl.message(
|
||||||
|
'Pink',
|
||||||
|
name: 'fontColorPink',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Red`
|
||||||
|
String get fontColorRed {
|
||||||
|
return Intl.message(
|
||||||
|
'Red',
|
||||||
|
name: 'fontColorRed',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Default background`
|
||||||
|
String get backgroundColorDefault {
|
||||||
|
return Intl.message(
|
||||||
|
'Default background',
|
||||||
|
name: 'backgroundColorDefault',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Gray background`
|
||||||
|
String get backgroundColorGray {
|
||||||
|
return Intl.message(
|
||||||
|
'Gray background',
|
||||||
|
name: 'backgroundColorGray',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Brown background`
|
||||||
|
String get backgroundColorBrown {
|
||||||
|
return Intl.message(
|
||||||
|
'Brown background',
|
||||||
|
name: 'backgroundColorBrown',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Orange background`
|
||||||
|
String get backgroundColorOrange {
|
||||||
|
return Intl.message(
|
||||||
|
'Orange background',
|
||||||
|
name: 'backgroundColorOrange',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Yellow background`
|
||||||
|
String get backgroundColorYellow {
|
||||||
|
return Intl.message(
|
||||||
|
'Yellow background',
|
||||||
|
name: 'backgroundColorYellow',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Green background`
|
||||||
|
String get backgroundColorGreen {
|
||||||
|
return Intl.message(
|
||||||
|
'Green background',
|
||||||
|
name: 'backgroundColorGreen',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Blue background`
|
||||||
|
String get backgroundColorBlue {
|
||||||
|
return Intl.message(
|
||||||
|
'Blue background',
|
||||||
|
name: 'backgroundColorBlue',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Purple background`
|
||||||
|
String get backgroundColorPurple {
|
||||||
|
return Intl.message(
|
||||||
|
'Purple background',
|
||||||
|
name: 'backgroundColorPurple',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Pink background`
|
||||||
|
String get backgroundColorPink {
|
||||||
|
return Intl.message(
|
||||||
|
'Pink background',
|
||||||
|
name: 'backgroundColorPink',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Red background`
|
||||||
|
String get backgroundColorRed {
|
||||||
|
return Intl.message(
|
||||||
|
'Red background',
|
||||||
|
name: 'backgroundColorRed',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppLocalizationDelegate
|
class AppLocalizationDelegate
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ColorOption {
|
||||||
|
const ColorOption({
|
||||||
|
required this.colorHex,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String colorHex;
|
||||||
|
final String name;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _ColorType {
|
||||||
|
font,
|
||||||
|
background,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ColorPicker extends StatefulWidget {
|
||||||
|
const ColorPicker({
|
||||||
|
super.key,
|
||||||
|
this.selectedFontColorHex,
|
||||||
|
this.selectedBackgroundColorHex,
|
||||||
|
required this.pickerBackgroundColor,
|
||||||
|
required this.fontColorOptions,
|
||||||
|
required this.backgroundColorOptions,
|
||||||
|
required this.pickerItemHoverColor,
|
||||||
|
required this.pickerItemTextColor,
|
||||||
|
required this.onSubmittedbackgroundColorHex,
|
||||||
|
required this.onSubmittedFontColorHex,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? selectedFontColorHex;
|
||||||
|
final String? selectedBackgroundColorHex;
|
||||||
|
final Color pickerBackgroundColor;
|
||||||
|
final Color pickerItemHoverColor;
|
||||||
|
final Color pickerItemTextColor;
|
||||||
|
final void Function(String color) onSubmittedbackgroundColorHex;
|
||||||
|
final void Function(String color) onSubmittedFontColorHex;
|
||||||
|
|
||||||
|
final List<ColorOption> fontColorOptions;
|
||||||
|
final List<ColorOption> backgroundColorOptions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ColorPicker> createState() => _ColorPickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ColorPickerState extends State<ColorPicker> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.pickerBackgroundColor,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 5,
|
||||||
|
spreadRadius: 1,
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
|
),
|
||||||
|
height: 250,
|
||||||
|
width: 220,
|
||||||
|
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// font color
|
||||||
|
_buildHeader('font color'),
|
||||||
|
// padding
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildColorItems(
|
||||||
|
_ColorType.font,
|
||||||
|
widget.fontColorOptions,
|
||||||
|
widget.selectedFontColorHex,
|
||||||
|
),
|
||||||
|
// background color
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildHeader('background color'),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildColorItems(
|
||||||
|
_ColorType.background,
|
||||||
|
widget.backgroundColorOptions,
|
||||||
|
widget.selectedBackgroundColorHex,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(String text) {
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildColorItems(
|
||||||
|
_ColorType type, List<ColorOption> options, String? selectedColor) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: options
|
||||||
|
.map((e) => _buildColorItem(type, e, e.colorHex == selectedColor))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildColorItem(_ColorType type, ColorOption option, bool isChecked) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 36,
|
||||||
|
child: InkWell(
|
||||||
|
customBorder: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
hoverColor: widget.pickerItemHoverColor,
|
||||||
|
onTap: () {
|
||||||
|
if (type == _ColorType.font) {
|
||||||
|
widget.onSubmittedFontColorHex(option.colorHex);
|
||||||
|
} else if (type == _ColorType.background) {
|
||||||
|
widget.onSubmittedbackgroundColorHex(option.colorHex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// padding
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
// icon
|
||||||
|
SizedBox.square(
|
||||||
|
dimension: 12,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(int.tryParse(option.colorHex) ?? 0xFFFFFFFF),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// padding
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
// text
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
option.name,
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 12, color: widget.pickerItemTextColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// checkbox
|
||||||
|
if (isChecked) const FlowySvg(name: 'checkmark'),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -255,6 +255,11 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||||
TextStyle(backgroundColor: attributes.backgroundColor),
|
TextStyle(backgroundColor: attributes.backgroundColor),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (attributes.color != null) {
|
||||||
|
textStyle = textStyle.combine(
|
||||||
|
TextStyle(color: attributes.color),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
offset += textInsert.length;
|
offset += textInsert.length;
|
||||||
textSpans.add(
|
textSpans.add(
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
|
||||||
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/infra/clipboard.dart';
|
import 'package:appflowy_editor/src/infra/clipboard.dart';
|
||||||
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||||
|
import 'package:appflowy_editor/src/render/color_menu/color_picker.dart';
|
||||||
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
|
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/text_node_extensions.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/extensions/editor_state_extensions.dart';
|
||||||
|
@ -264,6 +265,37 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||||
editorState.editorStyle.highlightColorHex!,
|
editorState.editorStyle.highlightColorHex!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ToolbarItem(
|
||||||
|
id: 'appflowy.toolbar.color',
|
||||||
|
type: 4,
|
||||||
|
tooltipsMessage: AppFlowyEditorLocalizations.current.color,
|
||||||
|
iconBuilder: (isHighlight) => Icon(
|
||||||
|
Icons.color_lens_outlined,
|
||||||
|
size: 14,
|
||||||
|
color: isHighlight ? Colors.lightBlue : Colors.white,
|
||||||
|
),
|
||||||
|
validator: _showInBuiltInTextSelection,
|
||||||
|
highlightCallback: (editorState) =>
|
||||||
|
_allSatisfy(
|
||||||
|
editorState,
|
||||||
|
BuiltInAttributeKey.color,
|
||||||
|
(value) =>
|
||||||
|
value != null &&
|
||||||
|
value != _generateFontColorOptions(editorState).first.colorHex,
|
||||||
|
) ||
|
||||||
|
_allSatisfy(
|
||||||
|
editorState,
|
||||||
|
BuiltInAttributeKey.backgroundColor,
|
||||||
|
(value) =>
|
||||||
|
value != null &&
|
||||||
|
value !=
|
||||||
|
_generateBackgroundColorOptions(editorState).first.colorHex,
|
||||||
|
),
|
||||||
|
handler: (editorState, context) => showColorMenu(
|
||||||
|
context,
|
||||||
|
editorState,
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) {
|
ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) {
|
||||||
|
@ -301,6 +333,8 @@ bool _allSatisfy(
|
||||||
}
|
}
|
||||||
|
|
||||||
OverlayEntry? _linkMenuOverlay;
|
OverlayEntry? _linkMenuOverlay;
|
||||||
|
OverlayEntry? _colorMenuOverlay;
|
||||||
|
|
||||||
EditorState? _editorState;
|
EditorState? _editorState;
|
||||||
bool _changeSelectionInner = false;
|
bool _changeSelectionInner = false;
|
||||||
void showLinkMenu(
|
void showLinkMenu(
|
||||||
|
@ -343,6 +377,7 @@ void showLinkMenu(
|
||||||
BuiltInAttributeKey.href,
|
BuiltInAttributeKey.href,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_linkMenuOverlay = OverlayEntry(builder: (context) {
|
_linkMenuOverlay = OverlayEntry(builder: (context) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: matchRect.bottom + 5.0,
|
top: matchRect.bottom + 5.0,
|
||||||
|
@ -360,6 +395,7 @@ void showLinkMenu(
|
||||||
text,
|
text,
|
||||||
textNode: textNode,
|
textNode: textNode,
|
||||||
);
|
);
|
||||||
|
|
||||||
_dismissLinkMenu();
|
_dismissLinkMenu();
|
||||||
},
|
},
|
||||||
onCopyLink: () {
|
onCopyLink: () {
|
||||||
|
@ -419,3 +455,211 @@ void _dismissLinkMenu() {
|
||||||
.removeListener(_dismissLinkMenu);
|
.removeListener(_dismissLinkMenu);
|
||||||
_editorState = null;
|
_editorState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _dismissColorMenu() {
|
||||||
|
// workaround: SelectionService has been released after hot reload.
|
||||||
|
final isSelectionDisposed =
|
||||||
|
_editorState?.service.selectionServiceKey.currentState == null;
|
||||||
|
if (isSelectionDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_editorState?.service.selectionService.currentSelection.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_changeSelectionInner) {
|
||||||
|
_changeSelectionInner = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_colorMenuOverlay?.remove();
|
||||||
|
_colorMenuOverlay = null;
|
||||||
|
|
||||||
|
_editorState?.service.scrollService?.enable();
|
||||||
|
_editorState?.service.keyboardService?.enable();
|
||||||
|
_editorState?.service.selectionService.currentSelection
|
||||||
|
.removeListener(_dismissColorMenu);
|
||||||
|
_editorState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showColorMenu(
|
||||||
|
BuildContext context,
|
||||||
|
EditorState editorState, {
|
||||||
|
Selection? customSelection,
|
||||||
|
}) {
|
||||||
|
final rects = editorState.service.selectionService.selectionRects;
|
||||||
|
var maxBottom = 0.0;
|
||||||
|
late Rect matchRect;
|
||||||
|
for (final rect in rects) {
|
||||||
|
if (rect.bottom > maxBottom) {
|
||||||
|
maxBottom = rect.bottom;
|
||||||
|
matchRect = rect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final baseOffset =
|
||||||
|
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||||
|
matchRect = matchRect.shift(-baseOffset);
|
||||||
|
|
||||||
|
_dismissColorMenu();
|
||||||
|
_editorState = editorState;
|
||||||
|
|
||||||
|
// Since the link menu will only show in single text selection,
|
||||||
|
// We get the text node directly instead of judging details again.
|
||||||
|
final selection = customSelection ??
|
||||||
|
editorState.service.selectionService.currentSelection.value;
|
||||||
|
|
||||||
|
final node = editorState.service.selectionService.currentSelectedNodes;
|
||||||
|
if (selection == null || node.isEmpty || node.first is! TextNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final textNode = node.first as TextNode;
|
||||||
|
|
||||||
|
String? backgroundColorHex;
|
||||||
|
if (textNode.allSatisfyBackgroundColorInSelection(selection)) {
|
||||||
|
backgroundColorHex = textNode.getAttributeInSelection<String>(
|
||||||
|
selection,
|
||||||
|
BuiltInAttributeKey.backgroundColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
String? fontColorHex;
|
||||||
|
if (textNode.allSatisfyFontColorInSelection(selection)) {
|
||||||
|
fontColorHex = textNode.getAttributeInSelection<String>(
|
||||||
|
selection,
|
||||||
|
BuiltInAttributeKey.color,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
fontColorHex = editorState.editorStyle.textStyle?.color?.toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
final style = editorState.editorStyle;
|
||||||
|
_colorMenuOverlay = OverlayEntry(builder: (context) {
|
||||||
|
return Positioned(
|
||||||
|
top: matchRect.bottom + 5.0,
|
||||||
|
left: matchRect.left + 10,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: ColorPicker(
|
||||||
|
pickerBackgroundColor:
|
||||||
|
style.selectionMenuBackgroundColor ?? Colors.white,
|
||||||
|
pickerItemHoverColor: style.selectionMenuItemSelectedColor ??
|
||||||
|
Colors.blue.withOpacity(0.3),
|
||||||
|
pickerItemTextColor: style.selectionMenuItemTextColor ?? Colors.black,
|
||||||
|
selectedFontColorHex: fontColorHex,
|
||||||
|
selectedBackgroundColorHex: backgroundColorHex,
|
||||||
|
fontColorOptions: _generateFontColorOptions(editorState),
|
||||||
|
backgroundColorOptions: _generateBackgroundColorOptions(editorState),
|
||||||
|
onSubmittedbackgroundColorHex: (color) {
|
||||||
|
formatHighlightColor(
|
||||||
|
editorState,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
_dismissColorMenu();
|
||||||
|
},
|
||||||
|
onSubmittedFontColorHex: (color) {
|
||||||
|
formatFontColor(
|
||||||
|
editorState,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
_dismissColorMenu();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Overlay.of(context)?.insert(_colorMenuOverlay!);
|
||||||
|
|
||||||
|
editorState.service.scrollService?.disable();
|
||||||
|
editorState.service.keyboardService?.disable();
|
||||||
|
editorState.service.selectionService.currentSelection
|
||||||
|
.addListener(_dismissColorMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ColorOption> _generateFontColorOptions(EditorState editorState) {
|
||||||
|
final defaultColor =
|
||||||
|
editorState.editorStyle.textStyle?.color ?? Colors.black; // black
|
||||||
|
return [
|
||||||
|
ColorOption(
|
||||||
|
colorHex: defaultColor.toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.fontColorDefault,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.grey.toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.fontColorGray,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.brown.toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.fontColorBrown,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.yellow.toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.fontColorYellow,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.green.toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.fontColorGreen,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.blue.toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.fontColorBlue,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.purple.toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.fontColorPurple,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.pink.toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.fontColorPink,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.red.toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.fontColorRed,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ColorOption> _generateBackgroundColorOptions(EditorState editorState) {
|
||||||
|
final defaultBackgroundColorHex =
|
||||||
|
editorState.editorStyle.highlightColorHex ?? '0x6000BCF0';
|
||||||
|
return [
|
||||||
|
ColorOption(
|
||||||
|
colorHex: defaultBackgroundColorHex,
|
||||||
|
name: AppFlowyEditorLocalizations.current.backgroundColorDefault,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.grey.withOpacity(0.3).toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.backgroundColorGray,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.brown.withOpacity(0.3).toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.backgroundColorBrown,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.yellow.withOpacity(0.3).toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.backgroundColorYellow,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.green.withOpacity(0.3).toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.backgroundColorGreen,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.blue.withOpacity(0.3).toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.backgroundColorBlue,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.purple.withOpacity(0.3).toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.backgroundColorPurple,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.pink.withOpacity(0.3).toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.backgroundColorPink,
|
||||||
|
),
|
||||||
|
ColorOption(
|
||||||
|
colorHex: Colors.red.withOpacity(0.3).toHex(),
|
||||||
|
name: AppFlowyEditorLocalizations.current.backgroundColorRed,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Color {
|
||||||
|
String toHex() {
|
||||||
|
return '0x${value.toRadixString(16)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -173,6 +173,22 @@ bool formatHighlight(EditorState editorState, String colorHex) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool formatHighlightColor(EditorState editorState, String colorHex) {
|
||||||
|
return formatRichTextPartialStyle(
|
||||||
|
editorState,
|
||||||
|
BuiltInAttributeKey.backgroundColor,
|
||||||
|
customValue: colorHex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool formatFontColor(EditorState editorState, String colorHex) {
|
||||||
|
return formatRichTextPartialStyle(
|
||||||
|
editorState,
|
||||||
|
BuiltInAttributeKey.color,
|
||||||
|
customValue: colorHex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool formatRichTextPartialStyle(EditorState editorState, String styleKey,
|
bool formatRichTextPartialStyle(EditorState editorState, String styleKey,
|
||||||
{Object? customValue}) {
|
{Object? customValue}) {
|
||||||
Attributes attributes = {
|
Attributes attributes = {
|
||||||
|
|
|
@ -50,14 +50,15 @@ void main() async {
|
||||||
null,
|
null,
|
||||||
delta: Delta()
|
delta: Delta()
|
||||||
..insert(
|
..insert(
|
||||||
'appflowy.io',
|
link,
|
||||||
attributes: {
|
attributes: {
|
||||||
BuiltInAttributeKey.href: link,
|
BuiltInAttributeKey.href: link,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await editor.startTesting();
|
await editor.startTesting();
|
||||||
final finder = find.byType(RichText);
|
await tester.pumpAndSettle();
|
||||||
|
final finder = find.text(link, findRichText: true);
|
||||||
expect(finder, findsOneWidget);
|
expect(finder, findsOneWidget);
|
||||||
|
|
||||||
// tap the link
|
// tap the link
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
|
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
|
||||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
|
import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
|
||||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import '../../infra/test_editor.dart';
|
import '../../infra/test_editor.dart';
|
||||||
|
|
||||||
|
@ -327,4 +328,51 @@ void main() async {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
group('toolbar, color picker', (() {
|
||||||
|
testWidgets(
|
||||||
|
'Select Text, Click Toolbar and set color for the selected text',
|
||||||
|
(tester) async {
|
||||||
|
final editor = tester.editor..insertTextNode(singleLineText);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final node = editor.nodeAtPath([0]) as TextNode;
|
||||||
|
final selection = Selection(
|
||||||
|
start: Position(path: [0], offset: 0),
|
||||||
|
end: Position(path: [0], offset: singleLineText.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
expect(find.byType(ToolbarWidget), findsOneWidget);
|
||||||
|
final colorButton = find.byWidgetPredicate((widget) {
|
||||||
|
if (widget is ToolbarItemWidget) {
|
||||||
|
return widget.item.id == 'appflowy.toolbar.color';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
expect(colorButton, findsOneWidget);
|
||||||
|
await tester.tap(colorButton);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// select a yellow color
|
||||||
|
final yellowButton = find.text('Yellow');
|
||||||
|
await tester.tap(yellowButton);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
node.allSatisfyInSelection(
|
||||||
|
selection,
|
||||||
|
BuiltInAttributeKey.color,
|
||||||
|
(value) {
|
||||||
|
return value == Colors.yellow.toHex();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Color {
|
||||||
|
String toHex() {
|
||||||
|
return '0x${value.toRadixString(16)}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue