mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00: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
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
|
||||
with:
|
||||
name: appflowy_editor
|
||||
flags: appflowy editor
|
||||
flags: appflowy_editor
|
||||
env_vars: ${{ matrix.os }}
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
@ -96,7 +96,8 @@
|
||||
"inlineCode": "Inline Code",
|
||||
"quote": "Quote Block",
|
||||
"header": "Header",
|
||||
"highlight": "Highlight"
|
||||
"highlight": "Highlight",
|
||||
"color": "Color"
|
||||
},
|
||||
"tooltip": {
|
||||
"lightMode": "Switch to Light mode",
|
||||
|
@ -90,7 +90,8 @@
|
||||
"inlineCode": "Código embebido",
|
||||
"quote": "Cita",
|
||||
"header": "Título",
|
||||
"highlight": "Resaltado"
|
||||
"highlight": "Resaltado",
|
||||
"color": "Color"
|
||||
},
|
||||
"tooltip": {
|
||||
"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": {},
|
||||
"highlight": "Highlight",
|
||||
"@highlight": {},
|
||||
"color": "Color",
|
||||
"@color": {},
|
||||
"image": "Image",
|
||||
"@image": {},
|
||||
"italic": "Italic",
|
||||
@ -31,5 +33,45 @@
|
||||
"text": "Text",
|
||||
"@text": {},
|
||||
"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.strikethrough,
|
||||
BuiltInAttributeKey.backgroundColor,
|
||||
BuiltInAttributeKey.color,
|
||||
BuiltInAttributeKey.href,
|
||||
BuiltInAttributeKey.code,
|
||||
];
|
||||
|
@ -34,6 +34,17 @@ extension TextNodeExtension on TextNode {
|
||||
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) =>
|
||||
allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) {
|
||||
return value == true;
|
||||
|
@ -22,10 +22,41 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
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"),
|
||||
"bulletedList": MessageLookupByLibrary.simpleMessage("Bulleted List"),
|
||||
"checkbox": MessageLookupByLibrary.simpleMessage("Checkbox"),
|
||||
"color": MessageLookupByLibrary.simpleMessage("Color"),
|
||||
"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"),
|
||||
"heading2": MessageLookupByLibrary.simpleMessage("H2"),
|
||||
"heading3": MessageLookupByLibrary.simpleMessage("H3"),
|
||||
|
@ -131,6 +131,16 @@ class AppFlowyEditorLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Color`
|
||||
String get color {
|
||||
return Intl.message(
|
||||
'Color',
|
||||
name: 'color',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Image`
|
||||
String get image {
|
||||
return Intl.message(
|
||||
@ -210,6 +220,206 @@ class AppFlowyEditorLocalizations {
|
||||
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
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
if (attributes.color != null) {
|
||||
textStyle = textStyle.combine(
|
||||
TextStyle(color: attributes.color),
|
||||
);
|
||||
}
|
||||
}
|
||||
offset += textInsert.length;
|
||||
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/infra/clipboard.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/extensions/text_node_extensions.dart';
|
||||
import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart';
|
||||
@ -264,6 +265,37 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||
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) {
|
||||
@ -301,6 +333,8 @@ bool _allSatisfy(
|
||||
}
|
||||
|
||||
OverlayEntry? _linkMenuOverlay;
|
||||
OverlayEntry? _colorMenuOverlay;
|
||||
|
||||
EditorState? _editorState;
|
||||
bool _changeSelectionInner = false;
|
||||
void showLinkMenu(
|
||||
@ -343,6 +377,7 @@ void showLinkMenu(
|
||||
BuiltInAttributeKey.href,
|
||||
);
|
||||
}
|
||||
|
||||
_linkMenuOverlay = OverlayEntry(builder: (context) {
|
||||
return Positioned(
|
||||
top: matchRect.bottom + 5.0,
|
||||
@ -360,6 +395,7 @@ void showLinkMenu(
|
||||
text,
|
||||
textNode: textNode,
|
||||
);
|
||||
|
||||
_dismissLinkMenu();
|
||||
},
|
||||
onCopyLink: () {
|
||||
@ -419,3 +455,211 @@ void _dismissLinkMenu() {
|
||||
.removeListener(_dismissLinkMenu);
|
||||
_editorState = null;
|
||||
}
|
||||
|
||||
void _dismissColorMenu() {
|
||||
// workaround: SelectionService has been released after hot reload.
|
||||
final isSelectionDisposed =
|
||||
_editorState?.service.selectionServiceKey.currentState == null;
|
||||
if (isSelectionDisposed) {
|
||||
return;
|
||||
}
|
||||
if (_editorState?.service.selectionService.currentSelection.value == null) {
|
||||
return;
|
||||
}
|
||||
if (_changeSelectionInner) {
|
||||
_changeSelectionInner = false;
|
||||
return;
|
||||
}
|
||||
_colorMenuOverlay?.remove();
|
||||
_colorMenuOverlay = null;
|
||||
|
||||
_editorState?.service.scrollService?.enable();
|
||||
_editorState?.service.keyboardService?.enable();
|
||||
_editorState?.service.selectionService.currentSelection
|
||||
.removeListener(_dismissColorMenu);
|
||||
_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,
|
||||
{Object? customValue}) {
|
||||
Attributes attributes = {
|
||||
|
@ -50,14 +50,15 @@ void main() async {
|
||||
null,
|
||||
delta: Delta()
|
||||
..insert(
|
||||
'appflowy.io',
|
||||
link,
|
||||
attributes: {
|
||||
BuiltInAttributeKey.href: link,
|
||||
},
|
||||
),
|
||||
);
|
||||
await editor.startTesting();
|
||||
final finder = find.byType(RichText);
|
||||
await tester.pumpAndSettle();
|
||||
final finder = find.text(link, findRichText: true);
|
||||
expect(finder, findsOneWidget);
|
||||
|
||||
// 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/render/toolbar/toolbar_item_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 '../../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…
Reference in New Issue
Block a user