Merge remote-tracking branch 'origin/main' into tekdel/main

# Conflicts:
#	frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart
This commit is contained in:
Lucas.Xu 2022-10-08 11:10:20 +08:00
commit e9c0956c51
113 changed files with 2208 additions and 535 deletions

View File

@ -1,4 +1,4 @@
name: FlowyEditor test
name: AppFlowyEditor test
on:
push:
@ -37,6 +37,8 @@ jobs:
working-directory: frontend/app_flowy/packages/appflowy_editor
run: |
flutter pub get
flutter format --set-exit-if-changed .
flutter analyze .
flutter test --coverage
- uses: codecov/codecov-action@v3

View File

@ -94,7 +94,20 @@
},
"tooltip": {
"lightMode": "Passer en mode clair",
"darkMode": "Passer en mode sombre"
"darkMode": "Passer en mode sombre",
"openAsPage": "Ouvrir en tant que page",
"addNewRow": "Ajouter une ligne",
"openMenu": "Cliquer pour ouvrir le menu"
},
"sideBar": {
"closeSidebar": "Fermer le menu latéral",
"openSidebar": "Ouvrir le menu latéral"
},
"notifications": {
"export": {
"markdown": "Note exportée en Markdown",
"path": "Documents/flowy"
}
},
"contactsPage": {
"title": "Contacts",
@ -123,7 +136,7 @@
"failedMsg": "Assurez-vous d'avoir terminé le processus de connexion dans votre navigateur."
},
"google": {
"title": "CONNEXION GOOGLE",
"title": "CONNEXION VIA GOOGLE",
"instruction1": "Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur web.",
"instruction2": "Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte:",
"instruction3": "Accédez au lien suivant dans votre navigateur web et saisissez le code ci-dessus:",
@ -135,6 +148,7 @@
"menu": {
"appearance": "Apparence",
"language": "Langue",
"user": "Utilisateur",
"open": "Ouvrir les paramètres"
},
"appearance": {
@ -142,15 +156,12 @@
"darkLabel": "Mode sombre"
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
},
"grid": {
"settings": {
"filter": "Filtrer",
"sortBy": "Filtrer par",
"Properties": "Propriétés"
"Properties": "Propriétés",
"group": "Groupe"
},
"field": {
"hide": "Cacher",
@ -179,13 +190,17 @@
"addSelectOption": "Ajouter une option",
"optionTitle": "Options",
"addOption": "Ajouter une option",
"editProperty": "Modifier la propriété"
"editProperty": "Modifier la propriété",
"newColumn": "Nouvelle colonne",
"deleteFieldPromptMessage": "Vous voulez supprimer cette propriété ?"
},
"row": {
"duplicate": "Dupliquer",
"delete": "Supprimer",
"textPlaceholder": "Vide",
"copyProperty": "Copie de la propriété dans le presse-papiers"
"copyProperty": "Copie de la propriété dans le presse-papiers",
"count": "Nombre",
"newRow": "Nouvelle ligne"
},
"selectOption": {
"create": "Créer",
@ -211,5 +226,10 @@
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
}
},
"board": {
"column": {
"create_new_card": "Nouveau"
}
}
}
}

View File

@ -45,5 +45,7 @@
<array>
<string>en</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -89,18 +89,30 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
(err) => Log.error(err),
);
},
didCreateRow: (String groupId, RowPB row, int? index) {
didCreateRow: (group, row, int? index) {
emit(state.copyWith(
editingRow: Some(BoardEditingRow(
columnId: groupId,
group: group,
row: row,
index: index,
)),
));
_groupItemStartEditing(group, row, true);
},
endEditRow: (rowId) {
startEditingRow: (group, row) {
emit(state.copyWith(
editingRow: Some(BoardEditingRow(
group: group,
row: row,
index: null,
)),
));
_groupItemStartEditing(group, row, true);
},
endEditingRow: (rowId) {
state.editingRow.fold(() => null, (editingRow) {
assert(editingRow.row.id == rowId);
_groupItemStartEditing(editingRow.group, editingRow.row, false);
emit(state.copyWith(editingRow: none()));
});
},
@ -122,6 +134,24 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
}
void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
final fieldContext = fieldController.getField(group.fieldId);
if (fieldContext == null) {
Log.warn("FieldContext should not be null");
return;
}
boardController.enableGroupDragging(!isEdit);
// boardController.updateGroupItem(
// group.groupId,
// GroupItem(
// row: row,
// fieldContext: fieldContext,
// isDraggable: !isEdit,
// ),
// );
}
void _moveRow(RowPB? fromRow, String columnId, RowPB? toRow) {
if (fromRow != null) {
_rowService
@ -136,11 +166,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
}
}
void _moveGroup(String fromColumnId, String toColumnId) {
void _moveGroup(String fromGroupId, String toGroupId) {
_rowService
.moveGroup(
fromGroupId: fromColumnId,
toGroupId: toColumnId,
fromGroupId: fromGroupId,
toGroupId: toGroupId,
)
.then((result) {
result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
@ -156,7 +186,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
return super.close();
}
void initializeGroups(List<GroupPB> groups) {
void initializeGroups(List<GroupPB> groupsData) {
for (var controller in groupControllers.values) {
controller.dispose();
}
@ -164,27 +194,27 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
boardController.clear();
//
List<AppFlowyGroupData> columns = groups
List<AppFlowyGroupData> groups = groupsData
.where((group) => fieldController.getField(group.fieldId) != null)
.map((group) {
return AppFlowyGroupData(
id: group.groupId,
name: group.desc,
items: _buildRows(group),
customData: BoardCustomData(
items: _buildGroupItems(group),
customData: GroupData(
group: group,
fieldContext: fieldController.getField(group.fieldId)!,
),
);
}).toList();
boardController.addGroups(columns);
boardController.addGroups(groups);
for (final group in groups) {
for (final group in groupsData) {
final delegate = GroupControllerDelegateImpl(
controller: boardController,
fieldController: fieldController,
onNewColumnItem: (groupId, row, index) {
add(BoardEvent.didCreateRow(groupId, row, index));
add(BoardEvent.didCreateRow(group, row, index));
},
);
final controller = GroupController(
@ -242,10 +272,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
}
List<AppFlowyGroupItem> _buildRows(GroupPB group) {
List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) {
final items = group.rows.map((row) {
final fieldContext = fieldController.getField(group.fieldId);
return BoardColumnItem(row: row, fieldContext: fieldContext!);
return GroupItem(
row: row,
fieldContext: fieldContext!,
);
}).toList();
return <AppFlowyGroupItem>[...items];
@ -270,11 +303,15 @@ class BoardEvent with _$BoardEvent {
const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
const factory BoardEvent.didCreateRow(
String groupId,
GroupPB group,
RowPB row,
int? index,
) = _DidCreateRow;
const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
const factory BoardEvent.startEditingRow(
GroupPB group,
RowPB row,
) = _StartEditRow;
const factory BoardEvent.endEditingRow(String rowId) = _EndEditRow;
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
const factory BoardEvent.didReceiveGridUpdate(
GridPB grid,
@ -334,14 +371,17 @@ class GridFieldEquatable extends Equatable {
UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
}
class BoardColumnItem extends AppFlowyGroupItem {
class GroupItem extends AppFlowyGroupItem {
final RowPB row;
final GridFieldContext fieldContext;
BoardColumnItem({
GroupItem({
required this.row,
required this.fieldContext,
});
bool draggable = true,
}) {
super.draggable = draggable;
}
@override
String get id => row.id;
@ -367,10 +407,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
}
if (index != null) {
final item = BoardColumnItem(row: row, fieldContext: fieldContext);
final item = GroupItem(
row: row,
fieldContext: fieldContext,
);
controller.insertGroupItem(group.groupId, index, item);
} else {
final item = BoardColumnItem(row: row, fieldContext: fieldContext);
final item = GroupItem(
row: row,
fieldContext: fieldContext,
);
controller.addGroupItem(group.groupId, item);
}
}
@ -389,7 +435,10 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
}
controller.updateGroupItem(
group.groupId,
BoardColumnItem(row: row, fieldContext: fieldContext),
GroupItem(
row: row,
fieldContext: fieldContext,
),
);
}
@ -400,7 +449,11 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
Log.warn("FieldContext should not be null");
return;
}
final item = BoardColumnItem(row: row, fieldContext: fieldContext);
final item = GroupItem(
row: row,
fieldContext: fieldContext,
draggable: false,
);
if (index != null) {
controller.insertGroupItem(group.groupId, index, item);
@ -412,21 +465,21 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
}
class BoardEditingRow {
String columnId;
GroupPB group;
RowPB row;
int? index;
BoardEditingRow({
required this.columnId,
required this.group,
required this.row,
required this.index,
});
}
class BoardCustomData {
class GroupData {
final GroupPB group;
final GridFieldContext fieldContext;
BoardCustomData({
GroupData({
required this.group,
required this.fieldContext,
});

View File

@ -87,13 +87,13 @@ class BoardDataController {
onUpdatedGroup.call(changeset.updateGroups);
}
if (changeset.insertedGroups.isNotEmpty) {
onInsertedGroup.call(changeset.insertedGroups);
}
if (changeset.deletedGroups.isNotEmpty) {
onDeletedGroup.call(changeset.deletedGroups);
}
if (changeset.insertedGroups.isNotEmpty) {
onInsertedGroup.call(changeset.insertedGroups);
}
},
(e) => _onError?.call(e),
);

View File

@ -83,7 +83,7 @@ class _BoardContentState extends State<BoardContent> {
@override
Widget build(BuildContext context) {
return BlocListener<BoardBloc, BoardState>(
listener: (context, state) => _handleEditState(state, context),
listener: (context, state) => _handleEditStateChanged(state, context),
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
builder: (context, state) {
@ -128,21 +128,14 @@ class _BoardContentState extends State<BoardContent> {
);
}
void _handleEditState(BoardState state, BuildContext context) {
void _handleEditStateChanged(BoardState state, BuildContext context) {
state.editingRow.fold(
() => null,
(editingRow) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (editingRow.index != null) {
context
.read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id));
} else {
scrollManager.scrollToBottom(editingRow.columnId, (boardContext) {
context
.read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id));
});
scrollManager.scrollToBottom(editingRow.group.groupId);
}
});
},
@ -156,14 +149,14 @@ class _BoardContentState extends State<BoardContent> {
Widget _buildHeader(
BuildContext context,
AppFlowyGroupData columnData,
AppFlowyGroupData groupData,
) {
final boardCustomData = columnData.customData as BoardCustomData;
final boardCustomData = groupData.customData as GroupData;
return AppFlowyGroupHeader(
title: Flexible(
fit: FlexFit.tight,
child: FlowyText.medium(
columnData.headerData.groupName,
groupData.headerData.groupName,
fontSize: 14,
overflow: TextOverflow.clip,
color: context.read<AppTheme>().textColor,
@ -180,7 +173,7 @@ class _BoardContentState extends State<BoardContent> {
),
onAddButtonClick: () {
context.read<BoardBloc>().add(
BoardEvent.createHeaderRow(columnData.id),
BoardEvent.createHeaderRow(groupData.id),
);
},
height: 50,
@ -218,15 +211,16 @@ class _BoardContentState extends State<BoardContent> {
Widget _buildCard(
BuildContext context,
AppFlowyGroupData group,
AppFlowyGroupItem columnItem,
AppFlowyGroupData afGroupData,
AppFlowyGroupItem afGroupItem,
) {
final boardColumnItem = columnItem as BoardColumnItem;
final rowPB = boardColumnItem.row;
final groupItem = afGroupItem as GroupItem;
final groupData = afGroupData.customData as GroupData;
final rowPB = groupItem.row;
final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox(key: ObjectKey(columnItem));
if (rowCache == null) return SizedBox(key: ObjectKey(groupItem));
final fieldController = context.read<BoardBloc>().fieldController;
final gridId = context.read<BoardBloc>().gridId;
@ -241,19 +235,19 @@ class _BoardContentState extends State<BoardContent> {
context.read<BoardBloc>().state.editingRow.fold(
() => null,
(editingRow) {
isEditing = editingRow.row.id == columnItem.row.id;
isEditing = editingRow.row.id == groupItem.row.id;
},
);
final groupItemId = columnItem.id + group.id;
final groupItemId = groupItem.row.id + groupData.group.groupId;
return AppFlowyGroupCard(
key: ValueKey(groupItemId),
margin: config.cardPadding,
decoration: _makeBoxDecoration(context),
child: BoardCard(
gridId: gridId,
groupId: group.id,
fieldId: boardColumnItem.fieldContext.id,
groupId: groupData.group.groupId,
fieldId: groupItem.fieldContext.id,
isEditing: isEditing,
cellBuilder: cellBuilder,
dataController: cardController,
@ -264,6 +258,19 @@ class _BoardContentState extends State<BoardContent> {
rowCache,
context,
),
onStartEditing: () {
context.read<BoardBloc>().add(
BoardEvent.startEditingRow(
groupData.group,
groupItem.row,
),
);
},
onEndEditing: () {
context
.read<BoardBloc>()
.add(BoardEvent.endEditingRow(groupItem.row.id));
},
),
);
}
@ -345,7 +352,7 @@ extension HexColor on Color {
}
}
Widget? _buildHeaderIcon(BoardCustomData customData) {
Widget? _buildHeaderIcon(GroupData customData) {
Widget? widget;
switch (customData.fieldType) {
case FieldType.Checkbox:

View File

@ -76,6 +76,10 @@ class EditableRowNotifier {
}
abstract class EditableCell {
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
// the row notifier receive its cells event. For example: begin editing the
// cell or end editing the cell.
//
EditableCellNotifier? get editableNotifier;
}

View File

@ -42,6 +42,9 @@ class _BoardTextCellState extends State<BoardTextCell> {
focusNode.requestFocus();
}
// If the focusNode lost its focus, the widget's editableNotifier will
// set to false, which will cause the [EditableRowNotifier] to receive
// end edit event.
focusNode.addListener(() {
if (!focusNode.hasFocus) {
focusWhenInit = false;
@ -131,7 +134,11 @@ class _BoardTextCellState extends State<BoardTextCell> {
padding: EdgeInsets.symmetric(
vertical: BoardSizes.cardCellVPadding,
),
child: FlowyText.medium(state.content, fontSize: 14),
child: FlowyText.medium(
state.content,
fontSize: 14,
maxLines: null, // Enable multiple lines
),
);
}

View File

@ -21,6 +21,8 @@ class BoardCard extends StatefulWidget {
final CardDataController dataController;
final BoardCellBuilder cellBuilder;
final void Function(BuildContext) openCard;
final VoidCallback onStartEditing;
final VoidCallback onEndEditing;
const BoardCard({
required this.gridId,
@ -30,6 +32,8 @@ class BoardCard extends StatefulWidget {
required this.dataController,
required this.cellBuilder,
required this.openCard,
required this.onStartEditing,
required this.onEndEditing,
Key? key,
}) : super(key: key);
@ -56,6 +60,12 @@ class _BoardCardState extends State<BoardCard> {
rowNotifier.isEditing.addListener(() {
if (!mounted) return;
_cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value));
if (rowNotifier.isEditing.value) {
widget.onStartEditing();
} else {
widget.onEndEditing();
}
});
popoverController = PopoverController();

View File

@ -15,6 +15,7 @@ import 'package:clipboard/clipboard.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flowy_sdk/log.dart';
@ -112,7 +113,6 @@ class DocumentShareButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
double buttonWidth = 60;
return BlocProvider(
create: (context) => getIt<DocShareBloc>(param1: view),
child: BlocListener<DocShareBloc, DocShareState>(
@ -130,6 +130,7 @@ class DocumentShareButton extends StatelessWidget {
},
child: BlocBuilder<DocShareBloc, DocShareState>(
builder: (context, state) {
final theme = context.watch<AppTheme>();
return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSetting, Locale>(
@ -137,16 +138,15 @@ class DocumentShareButton extends StatelessWidget {
builder: (ctx, _, child) => ConstrainedBox(
constraints: const BoxConstraints.expand(
height: 30,
// minWidth: buttonWidth,
width: 100,
),
child: RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(),
fontSize: 12,
borderRadius: Corners.s6Border,
color: Colors.lightBlue,
onPressed: () => _showActionList(
context, Offset(-(buttonWidth / 2), 10)),
color: theme.main1,
onPressed: () =>
_showActionList(context, const Offset(0, 10)),
),
),
),
@ -193,7 +193,7 @@ class DocumentShareButton extends StatelessWidget {
});
actionList.show(
context,
anchorDirection: AnchorDirection.bottomWithCenterAligned,
anchorDirection: AnchorDirection.bottomWithRightAligned,
anchorOffset: offset,
);
}

View File

@ -1,6 +1,8 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/cell/date_cal_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show Either;
import 'package:easy_localization/easy_localization.dart';
@ -157,6 +159,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
focusedDay: state.focusedDay,
rowHeight: 40,
calendarFormat: state.format,
daysOfWeekHeight: 40,
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
@ -166,15 +169,46 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
rightChevronPadding: EdgeInsets.zero,
rightChevronMargin: EdgeInsets.zero,
rightChevronIcon: svgWidget("home/arrow_right"),
headerMargin: const EdgeInsets.only(bottom: 8.0),
),
daysOfWeekStyle: DaysOfWeekStyle(
dowTextFormatter: (date, locale) =>
DateFormat.E(locale).format(date).toUpperCase(),
weekdayStyle: TextStyle(
fontSize: 13,
color: theme.shader3,
),
weekendStyle: TextStyle(
fontSize: 13,
color: theme.shader3,
),
),
calendarStyle: CalendarStyle(
cellMargin: const EdgeInsets.all(3),
defaultDecoration: BoxDecoration(
color: theme.surface,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
selectedDecoration: BoxDecoration(
color: theme.main1,
shape: BoxShape.circle,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
todayDecoration: BoxDecoration(
color: theme.shader4,
shape: BoxShape.circle,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
weekendDecoration: BoxDecoration(
color: theme.surface,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
outsideDecoration: BoxDecoration(
color: theme.surface,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
selectedTextStyle: TextStyle(
color: theme.surface,
@ -230,11 +264,13 @@ class _IncludeTimeButton extends StatelessWidget {
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(),
fontSize: 14),
const Spacer(),
Switch(
Toggle(
value: includeTime,
onChanged: (newValue) => context
onChanged: (value) => context
.read<DateCalBloc>()
.add(DateCalEvent.setIncludeTime(newValue)),
.add(DateCalEvent.setIncludeTime(!value)),
style: ToggleStyle.big(theme),
padding: EdgeInsets.zero,
),
],
),
@ -350,7 +386,7 @@ class _DateTypeOptionButton extends StatelessWidget {
offset: const Offset(20, 0),
constraints: BoxConstraints.loose(const Size(140, 100)),
child: FlowyButton(
text: FlowyText.medium(title, fontSize: 12),
text: FlowyText.medium(title, fontSize: 14),
hoverColor: theme.hover,
margin: kMargin,
rightIcon: svgWidget("grid/more", color: theme.iconColor),

View File

@ -130,14 +130,10 @@ class SelectOptionTagCell extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
fit: FlexFit.loose,
flex: 2,
child: SelectOptionTag.fromOption(
context: context,
option: option,
onSelected: () => onSelected(option),
),
SelectOptionTag.fromOption(
context: context,
option: option,
onSelected: () => onSelected(option),
),
const Spacer(),
...children,

View File

@ -142,11 +142,12 @@ class _TextField extends StatelessWidget {
value: (option) => option);
return SizedBox(
height: 42,
height: 62,
child: SelectOptionTextField(
options: state.options,
selectedOptionMap: optionMap,
distanceToText: _editorPanelWidth * 0.7,
maxLength: 30,
tagController: _tagController,
onClick: () => popoverMutex.close(),
newText: (text) {
@ -248,32 +249,25 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
mutex: widget.popoverMutex,
child: SizedBox(
height: GridSize.typeOptionItemHeight,
child: Row(
child: SelectOptionTagCell(
option: widget.option,
onSelected: (option) {
context
.read<SelectOptionCellEditorBloc>()
.add(SelectOptionEditorEvent.selectOption(option.id));
},
children: [
Flexible(
fit: FlexFit.loose,
child: SelectOptionTagCell(
option: widget.option,
onSelected: (option) {
context
.read<SelectOptionCellEditorBloc>()
.add(SelectOptionEditorEvent.selectOption(option.id));
},
children: [
if (widget.isSelected)
Padding(
padding: const EdgeInsets.only(right: 6),
child: svgWidget("grid/checkmark"),
),
],
if (widget.isSelected)
Padding(
padding: const EdgeInsets.only(right: 6),
child: svgWidget("grid/checkmark"),
),
),
FlowyIconButton(
width: 30,
onPressed: () => _popoverController.show(),
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
icon: svgWidget("editor/details", color: theme.iconColor),
)
),
],
),
),

View File

@ -6,6 +6,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:textfield_tags/textfield_tags.dart';
@ -20,6 +21,7 @@ class SelectOptionTextField extends StatefulWidget {
final Function(String) onSubmitted;
final Function(String) newText;
final VoidCallback? onClick;
final int? maxLength;
const SelectOptionTextField({
required this.options,
@ -29,6 +31,7 @@ class SelectOptionTextField extends StatefulWidget {
required this.onSubmitted,
required this.newText,
this.onClick,
this.maxLength,
TextEditingController? textController,
FocusNode? focusNode,
Key? key,
@ -93,6 +96,9 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
}
},
maxLines: 1,
maxLength: widget.maxLength,
maxLengthEnforcement:
MaxLengthEnforcement.truncateAfterCompositionEnds,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(

View File

@ -9,6 +9,7 @@ class InputTextField extends StatefulWidget {
final void Function() onCanceled;
final bool autoClearWhenDone;
final String text;
final int? maxLength;
const InputTextField({
required this.text,
@ -16,6 +17,7 @@ class InputTextField extends StatefulWidget {
required this.onCanceled,
this.onChanged,
this.autoClearWhenDone = false,
this.maxLength,
Key? key,
}) : super(key: key);
@ -41,11 +43,14 @@ class _InputTextFieldState extends State<InputTextField> {
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
final height = widget.maxLength == null ? 36.0 : 56.0;
return RoundedInputField(
controller: _controller,
focusNode: _focusNode,
autoFocus: true,
height: 36,
height: height,
maxLength: widget.maxLength,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
normalBorderColor: theme.shader4,
focusBorderColor: theme.main1,

View File

@ -1,5 +1,7 @@
import 'package:app_flowy/plugins/grid/application/field/type_option/date_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:easy_localization/easy_localization.dart' hide DateFormat;
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:flowy_infra/image.dart';
@ -161,6 +163,7 @@ class _IncludeTimeButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return BlocSelector<DateTypeOptionBloc, DateTypeOptionState, bool>(
selector: (state) => state.typeOption.includeTime,
builder: (context, includeTime) {
@ -173,13 +176,15 @@ class _IncludeTimeButton extends StatelessWidget {
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(),
fontSize: 12),
const Spacer(),
Switch(
Toggle(
value: includeTime,
onChanged: (newValue) {
onChanged: (value) {
context
.read<DateTypeOptionBloc>()
.add(DateTypeOptionEvent.includeTime(newValue));
.add(DateTypeOptionEvent.includeTime(!value));
},
style: ToggleStyle.big(theme),
padding: EdgeInsets.zero,
),
],
),

View File

@ -256,6 +256,7 @@ class _CreateOptionTextField extends StatelessWidget {
final text = state.newOptionName.foldRight("", (a, previous) => a);
return InputTextField(
autoClearWhenDone: true,
maxLength: 30,
text: text,
onCanceled: () {
context

View File

@ -106,6 +106,7 @@ class _OptionNameTextField extends StatelessWidget {
Widget build(BuildContext context) {
return InputTextField(
text: name,
maxLength: 30,
onCanceled: () {},
onDone: (optionName) {
if (name != optionName) {

View File

@ -1,5 +1,6 @@
import 'package:app_flowy/user/application/user_listener.dart';
import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
@ -50,13 +51,24 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
unauthorized: (_Unauthorized value) {
emit(state.copyWith(unauthorized: true));
},
collapseMenu: (e) {
collapseMenu: (_CollapseMenu e) {
emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
},
editPanelResized: (e) {
final newOffset =
(state.resizeOffset + e.offset).clamp(-50, 200).toDouble();
emit(state.copyWith(resizeOffset: newOffset));
editPanelResizeStart: (_EditPanelResizeStart e) {
emit(state.copyWith(
resizeType: MenuResizeType.drag,
resizeStart: state.resizeOffset,
));
},
editPanelResized: (_EditPanelResized e) {
final newPosition =
(e.offset + state.resizeStart).clamp(-50, 200).toDouble();
if (state.resizeOffset != newPosition) {
emit(state.copyWith(resizeOffset: newPosition));
}
},
editPanelResizeEnd: (_EditPanelResizeEnd e) {
emit(state.copyWith(resizeType: MenuResizeType.slide));
},
);
},
@ -78,6 +90,22 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
}
}
enum MenuResizeType {
slide,
drag,
}
extension MenuResizeTypeExtension on MenuResizeType {
Duration duration() {
switch (this) {
case MenuResizeType.drag:
return 30.milliseconds;
case MenuResizeType.slide:
return 350.milliseconds;
}
}
}
@freezed
class HomeEvent with _$HomeEvent {
const factory HomeEvent.initial() = _Initial;
@ -91,6 +119,8 @@ class HomeEvent with _$HomeEvent {
const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
const factory HomeEvent.collapseMenu() = _CollapseMenu;
const factory HomeEvent.editPanelResized(double offset) = _EditPanelResized;
const factory HomeEvent.editPanelResizeStart() = _EditPanelResizeStart;
const factory HomeEvent.editPanelResizeEnd() = _EditPanelResizeEnd;
}
@freezed
@ -103,6 +133,8 @@ class HomeState with _$HomeState {
required bool unauthorized,
required bool isMenuCollapsed,
required double resizeOffset,
required double resizeStart,
required MenuResizeType resizeType,
}) = _HomeState;
factory HomeState.initial(CurrentWorkspaceSettingPB workspaceSetting) =>
@ -114,5 +146,7 @@ class HomeState with _$HomeState {
unauthorized: false,
isMenuCollapsed: false,
resizeOffset: 0,
resizeStart: 0,
resizeType: MenuResizeType.slide,
);
}

View File

@ -2,7 +2,6 @@ import 'dart:io' show Platform;
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flutter/material.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:sized_context/sized_context.dart';
@ -44,7 +43,7 @@ class HomeLayout {
homePageLOffset = (showMenu && !menuIsDrawer) ? menuWidth : 0.0;
menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0;
animDuration = .35.seconds;
animDuration = homeBlocState.resizeType.duration();
editPanelWidth = HomeSizes.editPanelWidth;
homePageROffset = showEditPanel ? editPanelWidth : 0;

View File

@ -176,11 +176,18 @@ class _HomeScreenState extends State<HomeScreen> {
cursor: SystemMouseCursors.resizeLeftRight,
child: GestureDetector(
dragStartBehavior: DragStartBehavior.down,
onPanUpdate: ((details) {
context
.read<HomeBloc>()
.add(HomeEvent.editPanelResized(details.delta.dx));
}),
onHorizontalDragStart: (details) => context
.read<HomeBloc>()
.add(const HomeEvent.editPanelResizeStart()),
onHorizontalDragUpdate: (details) => context
.read<HomeBloc>()
.add(HomeEvent.editPanelResized(details.localPosition.dx)),
onHorizontalDragEnd: (details) => context
.read<HomeBloc>()
.add(const HomeEvent.editPanelResizeEnd()),
onHorizontalDragCancel: () => context
.read<HomeBloc>()
.add(const HomeEvent.editPanelResizeEnd()),
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 10,
@ -208,7 +215,6 @@ class _HomeScreenState extends State<HomeScreen> {
top: 0,
animate: true)
.animate(layout.animDuration, Curves.easeOut),
homeMenuResizer.positioned(left: layout.homePageLOffset - 5),
bubble
.positioned(
right: 20,
@ -236,6 +242,9 @@ class _HomeScreenState extends State<HomeScreen> {
bottom: 0,
animate: true)
.animate(layout.animDuration, Curves.easeOut),
homeMenuResizer
.positioned(left: layout.homePageLOffset - 5)
.animate(layout.animDuration, Curves.easeOut),
],
);
}

View File

@ -99,6 +99,7 @@ class MenuAppHeader extends StatelessWidget {
app.name,
fontSize: 12,
color: theme.textColor,
overflow: TextOverflow.ellipsis,
),
),
),

View File

@ -82,7 +82,7 @@ class ViewSectionItem extends StatelessWidget {
child: FlowyText.regular(
state.view.name,
fontSize: 12,
overflow: TextOverflow.clip,
overflow: TextOverflow.ellipsis,
),
),
];

View File

@ -28,8 +28,9 @@ class MenuUser extends StatelessWidget {
children: [
_renderAvatar(context),
const HSpace(10),
_renderUserName(context),
const Spacer(),
Expanded(
child: _renderUserName(context),
),
_renderSettingsButton(context),
//ToDo: when the user is allowed to create another workspace,
//we get the below block back
@ -63,7 +64,7 @@ class MenuUser extends StatelessWidget {
if (name.isEmpty) {
name = context.read<MenuUserBloc>().state.userProfile.email;
}
return FlowyText(name, fontSize: 12);
return FlowyText(name, fontSize: 12, overflow: TextOverflow.ellipsis);
}
Widget _renderSettingsButton(BuildContext context) {

View File

@ -13,7 +13,7 @@ class SettingsAppearanceView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();
final theme = context.watch<AppTheme>();
return SingleChildScrollView(
child: Column(

View File

@ -97,10 +97,8 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
child: SizedBox(
height: itemHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (icon != null) icon,
HSpace(ActionListSizes.itemHPadding),
if (icon != null) ...[icon, HSpace(ActionListSizes.itemHPadding)],
FlowyText.medium(action.name, fontSize: 12),
],
),

View File

@ -1,8 +1,14 @@
# 0.0.9
* Enable slide to select text in card
* Fix some bugs
# 0.0.8
* Enable drag and drop group
# 0.0.7
* Rename some classes
* Add documentation
# 0.0.6
* Support scroll to bottom
* Fix some bugs

View File

@ -78,7 +78,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
height: 50,
margin: config.groupItemPadding,
onAddButtonClick: () {
boardController.scrollToBottom(columnData.id, (p0) {});
boardController.scrollToBottom(columnData.id);
},
);
},

View File

@ -32,4 +32,8 @@ class Log {
'AppFlowyBoard: ❗️[Trace] - ${DateTime.now().second}=> $message');
}
}
static void error(String? message) {
debugPrint('AppFlowyBoard: ❌[Error] - ${DateTime.now().second}=> $message');
}
}

View File

@ -11,10 +11,11 @@ import 'reorder_phantom/phantom_controller.dart';
import '../rendering/board_overlay.dart';
class AppFlowyBoardScrollController {
AppFlowyBoardState? _groupState;
AppFlowyBoardState? _boardState;
void scrollToBottom(String groupId, void Function(BuildContext)? completed) {
_groupState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
void scrollToBottom(String groupId,
{void Function(BuildContext)? completed}) {
_boardState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
}
}
@ -39,9 +40,6 @@ class AppFlowyBoardConfig {
}
class AppFlowyBoard extends StatelessWidget {
/// The direction to use as the main axis.
final Axis direction = Axis.vertical;
/// The widget that will be rendered as the background of the board.
final Widget? background;
@ -94,11 +92,7 @@ class AppFlowyBoard extends StatelessWidget {
///
final AppFlowyBoardScrollController? boardScrollController;
final AppFlowyBoardState _groupState = AppFlowyBoardState();
late final BoardPhantomController _phantomController;
AppFlowyBoard({
const AppFlowyBoard({
required this.controller,
required this.cardBuilder,
this.background,
@ -109,12 +103,7 @@ class AppFlowyBoard extends StatelessWidget {
this.groupConstraints = const BoxConstraints(maxWidth: 200),
this.config = const AppFlowyBoardConfig(),
Key? key,
}) : super(key: key) {
_phantomController = BoardPhantomController(
delegate: controller,
groupsState: _groupState,
);
}
}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -122,8 +111,14 @@ class AppFlowyBoard extends StatelessWidget {
value: controller,
child: Consumer<AppFlowyBoardController>(
builder: (context, notifier, child) {
final boardState = AppFlowyBoardState();
BoardPhantomController phantomController = BoardPhantomController(
delegate: controller,
groupsState: boardState,
);
if (boardScrollController != null) {
boardScrollController!._groupState = _groupState;
boardScrollController!._boardState = boardState;
}
return _AppFlowyBoardContent(
@ -131,14 +126,14 @@ class AppFlowyBoard extends StatelessWidget {
dataController: controller,
scrollController: scrollController,
scrollManager: boardScrollController,
groupState: _groupState,
boardState: boardState,
background: background,
delegate: _phantomController,
delegate: phantomController,
groupConstraints: groupConstraints,
cardBuilder: cardBuilder,
footerBuilder: footerBuilder,
headerBuilder: headerBuilder,
phantomController: _phantomController,
phantomController: phantomController,
onReorder: controller.moveGroup,
);
},
@ -156,7 +151,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
final ReorderFlexConfig reorderFlexConfig;
final BoxConstraints groupConstraints;
final AppFlowyBoardScrollController? scrollManager;
final AppFlowyBoardState groupState;
final AppFlowyBoardState boardState;
final AppFlowyBoardCardBuilder cardBuilder;
final AppFlowyBoardHeaderBuilder? headerBuilder;
final AppFlowyBoardFooterBuilder? footerBuilder;
@ -169,7 +164,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
required this.delegate,
required this.dataController,
required this.scrollManager,
required this.groupState,
required this.boardState,
this.scrollController,
this.background,
required this.groupConstraints,
@ -178,7 +173,10 @@ class _AppFlowyBoardContent extends StatefulWidget {
this.headerBuilder,
required this.phantomController,
Key? key,
}) : reorderFlexConfig = const ReorderFlexConfig(),
}) : reorderFlexConfig = const ReorderFlexConfig(
direction: Axis.horizontal,
dragDirection: Axis.horizontal,
),
super(key: key);
@override
@ -198,7 +196,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
reorderFlexId: widget.dataController.identifier,
acceptedReorderFlexId: widget.dataController.groupIds,
delegate: widget.delegate,
columnsState: widget.groupState,
columnsState: widget.boardState,
);
final reorderFlex = ReorderFlex(
@ -206,9 +204,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
scrollController: widget.scrollController,
onReorder: widget.onReorder,
dataSource: widget.dataController,
direction: Axis.horizontal,
interceptor: interceptor,
reorderable: true,
children: _buildColumns(),
);
@ -254,7 +250,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
);
final reorderFlexAction = ReorderFlexActionImpl();
widget.groupState.reorderFlexActionMap[columnData.id] =
widget.boardState.reorderFlexActionMap[columnData.id] =
reorderFlexAction;
return ChangeNotifierProvider.value(
@ -275,8 +271,8 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
onReorder: widget.dataController.moveGroupItem,
cornerRadius: widget.config.cornerRadius,
backgroundColor: widget.config.groupBackgroundColor,
dragStateStorage: widget.groupState,
dragTargetKeys: widget.groupState,
dragStateStorage: widget.boardState,
dragTargetKeys: widget.boardState,
reorderFlexAction: reorderFlexAction,
);

View File

@ -138,7 +138,11 @@ class AppFlowyBoardController extends ChangeNotifier
/// groups or get ready to reinitialize the [AppFlowyBoard].
void clear() {
_groupDatas.clear();
for (final group in _groupControllers.values) {
group.dispose();
}
_groupControllers.clear();
notifyListeners();
}
@ -202,6 +206,14 @@ class AppFlowyBoardController extends ChangeNotifier
getGroupController(groupId)?.replaceOrInsertItem(item);
}
void enableGroupDragging(bool isEnable) {
for (var groupController in _groupControllers.values) {
groupController.enableDragging(isEnable);
}
notifyListeners();
}
/// Moves the item at [fromGroupIndex] in group with id [fromGroupId] to
/// group with id [toGroupId] at [toGroupIndex]
@override
@ -215,6 +227,8 @@ class AppFlowyBoardController extends ChangeNotifier
final fromGroupController = getGroupController(fromGroupId)!;
final toGroupController = getGroupController(toGroupId)!;
final fromGroupItem = fromGroupController.removeAt(fromGroupIndex);
if (fromGroupItem == null) return;
if (toGroupController.items.length > toGroupIndex) {
assert(toGroupController.items[toGroupIndex] is PhantomGroupItem);
@ -275,7 +289,9 @@ class AppFlowyBoardController extends ChangeNotifier
Log.trace(
'[$BoardPhantomController] update $groupId:$index to $groupId:$newIndex');
final item = groupController.removeAt(index, notify: false);
groupController.insert(newIndex, item, notify: false);
if (item != null) {
groupController.insert(newIndex, item, notify: false);
}
}
}
}

View File

@ -156,9 +156,9 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
widget.onDragStarted?.call(index);
},
onReorder: ((fromIndex, toIndex) {
if (widget.phantomController.isFromGroup(widget.groupId)) {
if (widget.phantomController.shouldReorder(widget.groupId)) {
widget.onReorder(widget.groupId, fromIndex, toIndex);
widget.phantomController.transformIndex(fromIndex, toIndex);
widget.phantomController.updateIndex(fromIndex, toIndex);
}
}),
onDragEnded: () {

View File

@ -5,6 +5,8 @@ import 'package:appflowy_board/src/widgets/reorder_flex/reorder_flex.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
typedef IsDraggable = bool;
/// A item represents the generic data model of each group card.
///
/// Each item displayed in the group required to implement this class.
@ -50,8 +52,17 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
/// * [notify] the default value of [notify] is true, it will notify the
/// listener. Set to false if you do not want to notify the listeners.
///
AppFlowyGroupItem removeAt(int index, {bool notify = true}) {
assert(index >= 0);
AppFlowyGroupItem? removeAt(int index, {bool notify = true}) {
if (groupData._items.length <= index) {
Log.error(
'Fatal error, index is out of bounds. Index: $index, len: ${groupData._items.length}');
return null;
}
if (index < 0) {
Log.error('Invalid index:$index');
return null;
}
Log.debug('[$AppFlowyGroupController] $groupData remove item at $index');
final item = groupData._items.removeAt(index);
@ -71,12 +82,17 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
/// Move the item from [fromIndex] to [toIndex]. It will do nothing if the
/// [fromIndex] equal to the [toIndex].
bool move(int fromIndex, int toIndex) {
assert(fromIndex >= 0);
assert(toIndex >= 0);
if (groupData._items.length < fromIndex) {
Log.error(
'Out of bounds error. index: $fromIndex should not greater than ${groupData._items.length}');
return false;
}
if (fromIndex == toIndex) {
return false;
}
Log.debug(
'[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex');
final item = groupData._items.removeAt(fromIndex);
@ -124,7 +140,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
Log.debug('[$AppFlowyGroupController] $groupData add $newItem');
} else {
if (index >= groupData._items.length) {
Log.warn(
Log.error(
'[$AppFlowyGroupController] unexpected items length, index should less than the count of the items. Index: $index, items count: ${items.length}');
return;
}
@ -155,6 +171,15 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
-1;
}
void enableDragging(bool isEnable) {
groupData.draggable = isEnable;
for (var item in groupData._items) {
item.draggable = isEnable;
}
_notify();
}
void _notify() {
notifyListeners();
}

View File

@ -16,13 +16,13 @@ class FlexDragTargetData extends DragTargetData {
@override
final int draggingIndex;
final DraggingState _state;
final DraggingState _draggingState;
Widget? get draggingWidget => _state.draggingWidget;
Widget? get draggingWidget => _draggingState.draggingWidget;
Size? get feedbackSize => _state.feedbackSize;
Size? get feedbackSize => _draggingState.feedbackSize;
bool get isDragging => _state.isDragging();
bool get isDragging => _draggingState.isDragging();
final String dragTargetId;
@ -40,8 +40,8 @@ class FlexDragTargetData extends DragTargetData {
required this.reorderFlexId,
required this.reorderFlexItem,
required this.dragTargetIndexKey,
required DraggingState state,
}) : _state = state;
required DraggingState draggingState,
}) : _draggingState = draggingState;
@override
String toString() {

View File

@ -1,3 +1,4 @@
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_board/src/utils/log.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@ -78,10 +79,12 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
final bool useMoveAnimation;
final bool draggable;
final IsDraggable draggable;
final double draggingOpacity;
final Axis? dragDirection;
const ReorderDragTarget({
Key? key,
required this.child,
@ -99,6 +102,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
this.onLeave,
this.draggableTargetBuilder,
this.draggingOpacity = 0.3,
this.dragDirection,
}) : super(key: key);
@override
@ -115,8 +119,10 @@ class _ReorderDragTargetState<T extends DragTargetData>
Widget dragTarget = DragTarget<T>(
builder: _buildDraggableWidget,
onWillAccept: (dragTargetData) {
assert(dragTargetData != null);
if (dragTargetData == null) return false;
if (dragTargetData == null) {
return false;
}
return widget.onWillAccept(dragTargetData);
},
onAccept: widget.onAccept,
@ -140,9 +146,6 @@ class _ReorderDragTargetState<T extends DragTargetData>
List<T?> acceptedCandidates,
List<dynamic> rejectedCandidates,
) {
if (!widget.draggable) {
return widget.child;
}
Widget feedbackBuilder = Builder(builder: (BuildContext context) {
BoxConstraints contentSizeConstraints =
BoxConstraints.loose(_draggingFeedbackSize!);
@ -163,7 +166,8 @@ class _ReorderDragTargetState<T extends DragTargetData>
widget.deleteAnimationController,
) ??
Draggable<DragTargetData>(
maxSimultaneousDrags: 1,
axis: widget.dragDirection,
maxSimultaneousDrags: widget.draggable ? 1 : 0,
data: widget.dragTargetData,
ignoringFeedbackSemantics: false,
feedback: feedbackBuilder,

View File

@ -1,6 +1,7 @@
import 'dart:collection';
import 'dart:math';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../../utils/log.dart';
@ -29,6 +30,8 @@ abstract class ReoderFlexDataSource {
abstract class ReoderFlexItem {
/// [id] is used to identify the item. It must be unique.
String get id;
IsDraggable draggable = true;
}
/// Cache each dragTarget's key.
@ -73,8 +76,15 @@ class ReorderFlexConfig {
final bool useMovePlaceholder;
/// [direction] How to place the children, default is Axis.vertical
final Axis direction;
final Axis? dragDirection;
const ReorderFlexConfig({
this.useMoveAnimation = true,
this.direction = Axis.vertical,
this.dragDirection,
}) : useMovePlaceholder = !useMoveAnimation;
}
@ -82,8 +92,6 @@ class ReorderFlex extends StatefulWidget {
final ReorderFlexConfig config;
final List<Widget> children;
/// [direction] How to place the children, default is Axis.vertical
final Axis direction;
final MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start;
final ScrollController? scrollController;
@ -108,8 +116,6 @@ class ReorderFlex extends StatefulWidget {
final ReorderFlexAction? reorderFlexAction;
final bool reorderable;
ReorderFlex({
Key? key,
this.scrollController,
@ -117,14 +123,12 @@ class ReorderFlex extends StatefulWidget {
required this.children,
required this.config,
required this.onReorder,
this.reorderable = true,
this.dragStateStorage,
this.dragTargetKeys,
this.onDragStarted,
this.onDragEnded,
this.interceptor,
this.reorderFlexAction,
this.direction = Axis.vertical,
}) : assert(children.every((Widget w) => w.key != null),
'All child must have a key.'),
super(key: key);
@ -146,8 +150,8 @@ class ReorderFlexState extends State<ReorderFlex>
/// Whether or not we are currently scrolling this view to show a widget.
bool _scrolling = false;
/// [dragState] records the dragging state including dragStartIndex, and phantomIndex, etc.
late DraggingState dragState;
/// [draggingState] records the dragging state including dragStartIndex, and phantomIndex, etc.
late DraggingState draggingState;
/// [_animation] controls the dragging animations
late DragTargetAnimation _animation;
@ -158,9 +162,9 @@ class ReorderFlexState extends State<ReorderFlex>
void initState() {
_notifier = ReorderFlexNotifier();
final flexId = widget.reorderFlexId;
dragState = widget.dragStateStorage?.readState(flexId) ??
draggingState = widget.dragStateStorage?.readState(flexId) ??
DraggingState(widget.reorderFlexId);
Log.trace('[DragTarget] init dragState: $dragState');
Log.trace('[DragTarget] init dragState: $draggingState');
widget.dragStateStorage?.removeState(flexId);
@ -168,7 +172,7 @@ class ReorderFlexState extends State<ReorderFlex>
reorderAnimationDuration: widget.config.reorderAnimationDuration,
entranceAnimateStatusChanged: (status) {
if (status == AnimationStatus.completed) {
if (dragState.nextIndex == -1) return;
if (draggingState.nextIndex == -1) return;
setState(() => _requestAnimationToNextIndex());
}
},
@ -225,7 +229,7 @@ class ReorderFlexState extends State<ReorderFlex>
indexKey,
);
children.add(_wrap(child, i, indexKey));
children.add(_wrap(child, i, indexKey, item.draggable));
// if (widget.config.useMovePlaceholder) {
// children.add(DragTargeMovePlaceholder(
@ -256,64 +260,70 @@ class ReorderFlexState extends State<ReorderFlex>
/// when the animation finish.
if (_animation.entranceController.isCompleted) {
dragState.removePhantom();
draggingState.removePhantom();
if (!isAcceptingNewTarget && dragState.didDragTargetMoveToNext()) {
if (!isAcceptingNewTarget && draggingState.didDragTargetMoveToNext()) {
return;
}
dragState.moveDragTargetToNext();
draggingState.moveDragTargetToNext();
_animation.animateToNext();
}
}
/// [child]: the child will be wrapped with dartTarget
/// [childIndex]: the index of the child in a list
Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) {
Widget _wrap(
Widget child,
int childIndex,
GlobalObjectKey indexKey,
IsDraggable draggable,
) {
return Builder(builder: (context) {
final ReorderDragTarget dragTarget = _buildDragTarget(
context,
child,
childIndex,
indexKey,
draggable,
);
int shiftedIndex = childIndex;
if (dragState.isOverlapWithPhantom()) {
shiftedIndex = dragState.calculateShiftedIndex(childIndex);
if (draggingState.isOverlapWithPhantom()) {
shiftedIndex = draggingState.calculateShiftedIndex(childIndex);
}
Log.trace(
'Rebuild: Group:[${dragState.reorderFlexId}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
final currentIndex = dragState.currentIndex;
final dragPhantomIndex = dragState.phantomIndex;
'Rebuild: Group:[${draggingState.reorderFlexId}] ${draggingState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
final currentIndex = draggingState.currentIndex;
final dragPhantomIndex = draggingState.phantomIndex;
if (shiftedIndex == currentIndex || childIndex == dragPhantomIndex) {
Widget dragSpace;
if (dragState.draggingWidget != null) {
if (dragState.draggingWidget is PhantomWidget) {
dragSpace = dragState.draggingWidget!;
if (draggingState.draggingWidget != null) {
if (draggingState.draggingWidget is PhantomWidget) {
dragSpace = draggingState.draggingWidget!;
} else {
dragSpace = PhantomWidget(
opacity: widget.config.draggingWidgetOpacity,
child: dragState.draggingWidget,
child: draggingState.draggingWidget,
);
}
} else {
dragSpace = SizedBox.fromSize(size: dragState.dropAreaSize);
dragSpace = SizedBox.fromSize(size: draggingState.dropAreaSize);
}
/// Returns the dragTarget it is not start dragging. The size of the
/// dragTarget is the same as the the passed in child.
///
if (dragState.isNotDragging()) {
if (draggingState.isNotDragging()) {
return _buildDraggingContainer(children: [dragTarget]);
}
/// Determine the size of the drop area to show under the dragging widget.
Size? feedbackSize = Size.zero;
if (widget.config.useMoveAnimation) {
feedbackSize = dragState.feedbackSize;
feedbackSize = draggingState.feedbackSize;
}
Widget appearSpace = _makeAppearSpace(dragSpace, feedbackSize);
@ -321,7 +331,7 @@ class ReorderFlexState extends State<ReorderFlex>
/// When start dragging, the dragTarget, [ReorderDragTarget], will
/// return a [IgnorePointerWidget] which size is zero.
if (dragState.isPhantomAboveDragTarget()) {
if (draggingState.isPhantomAboveDragTarget()) {
_notifier.updateDragTargetIndex(currentIndex);
if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
return _buildDraggingContainer(children: [
@ -343,7 +353,7 @@ class ReorderFlexState extends State<ReorderFlex>
}
///
if (dragState.isPhantomBelowDragTarget()) {
if (draggingState.isPhantomBelowDragTarget()) {
_notifier.updateDragTargetIndex(currentIndex);
if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
return _buildDraggingContainer(children: [
@ -364,10 +374,10 @@ class ReorderFlexState extends State<ReorderFlex>
}
}
assert(!dragState.isOverlapWithPhantom());
assert(!draggingState.isOverlapWithPhantom());
List<Widget> children = [];
if (dragState.isDragTargetMovingDown()) {
if (draggingState.isDragTargetMovingDown()) {
children.addAll([dragTarget, appearSpace]);
} else {
children.addAll([appearSpace, dragTarget]);
@ -395,15 +405,17 @@ class ReorderFlexState extends State<ReorderFlex>
Widget child,
int dragTargetIndex,
GlobalObjectKey indexKey,
IsDraggable draggable,
) {
final reorderFlexItem = widget.dataSource.items[dragTargetIndex];
return ReorderDragTarget<FlexDragTargetData>(
indexGlobalKey: indexKey,
draggable: draggable,
dragTargetData: FlexDragTargetData(
draggingIndex: dragTargetIndex,
reorderFlexId: widget.reorderFlexId,
reorderFlexItem: reorderFlexItem,
state: dragState,
draggingState: draggingState,
dragTargetId: reorderFlexItem.id,
dragTargetIndexKey: indexKey,
),
@ -432,11 +444,11 @@ class ReorderFlexState extends State<ReorderFlex>
setState(() {
if (dragTargetData.reorderFlexId == widget.reorderFlexId) {
_onReordered(
dragState.dragStartIndex,
dragState.currentIndex,
draggingState.dragStartIndex,
draggingState.currentIndex,
);
}
dragState.endDragging();
draggingState.endDragging();
widget.onDragEnded?.call();
});
},
@ -482,8 +494,8 @@ class ReorderFlexState extends State<ReorderFlex>
deleteAnimationController: _animation.deleteController,
draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
useMoveAnimation: widget.config.useMoveAnimation,
draggable: widget.reorderable,
draggingOpacity: widget.config.draggingWidgetOpacity,
dragDirection: widget.config.dragDirection,
child: child,
);
}
@ -506,7 +518,7 @@ class ReorderFlexState extends State<ReorderFlex>
child,
_animation.entranceController,
feedbackSize,
widget.direction,
widget.config.direction,
);
}
@ -515,7 +527,7 @@ class ReorderFlexState extends State<ReorderFlex>
child,
_animation.phantomController,
feedbackSize,
widget.direction,
widget.config.direction,
);
}
@ -525,7 +537,7 @@ class ReorderFlexState extends State<ReorderFlex>
Size? feedbackSize,
) {
setState(() {
dragState.startDragging(draggingWidget, dragIndex, feedbackSize);
draggingState.startDragging(draggingWidget, dragIndex, feedbackSize);
_animation.startDragging();
});
}
@ -535,34 +547,34 @@ class ReorderFlexState extends State<ReorderFlex>
return;
}
dragState.setStartDraggingIndex(dragTargetIndex);
draggingState.setStartDraggingIndex(dragTargetIndex);
widget.dragStateStorage?.insertState(
widget.reorderFlexId,
dragState,
draggingState,
);
}
bool handleOnWillAccept(BuildContext context, int dragTargetIndex) {
final dragIndex = dragState.dragStartIndex;
final dragIndex = draggingState.dragStartIndex;
/// The [willAccept] will be true if the dargTarget is the widget that gets
/// dragged and it is dragged on top of the other dragTargets.
///
bool willAccept =
dragState.dragStartIndex == dragIndex && dragIndex != dragTargetIndex;
bool willAccept = draggingState.dragStartIndex == dragIndex &&
dragIndex != dragTargetIndex;
setState(() {
if (willAccept) {
int shiftedIndex = dragState.calculateShiftedIndex(dragTargetIndex);
dragState.updateNextIndex(shiftedIndex);
int shiftedIndex = draggingState.calculateShiftedIndex(dragTargetIndex);
draggingState.updateNextIndex(shiftedIndex);
} else {
dragState.updateNextIndex(dragTargetIndex);
draggingState.updateNextIndex(dragTargetIndex);
}
_requestAnimationToNextIndex(isAcceptingNewTarget: true);
});
Log.trace(
'[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $dragState}');
'[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $draggingState}');
_scrollTo(context);
@ -587,7 +599,7 @@ class ReorderFlexState extends State<ReorderFlex>
return child;
} else {
return SingleChildScrollView(
scrollDirection: widget.direction,
scrollDirection: widget.config.direction,
controller: _scrollController,
child: child,
);
@ -595,7 +607,7 @@ class ReorderFlexState extends State<ReorderFlex>
}
Widget _wrapContainer(List<Widget> children) {
switch (widget.direction) {
switch (widget.config.direction) {
case Axis.horizontal:
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
@ -613,7 +625,7 @@ class ReorderFlexState extends State<ReorderFlex>
}
Widget _buildDraggingContainer({required List<Widget> children}) {
switch (widget.direction) {
switch (widget.config.direction) {
case Axis.horizontal:
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
@ -660,6 +672,7 @@ class ReorderFlexState extends State<ReorderFlex>
.ensureVisible(
dragTargetRenderObject,
alignment: 0.5,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
duration: const Duration(milliseconds: 120),
)
.then((value) {
@ -683,9 +696,9 @@ class ReorderFlexState extends State<ReorderFlex>
// If and only if the current scroll offset falls in-between the offsets
// necessary to reveal the selected context at the top or bottom of the
// screen, then it is already on-screen.
final double margin = widget.direction == Axis.horizontal
? dragState.dropAreaSize.width
: dragState.dropAreaSize.height / 2.0;
final double margin = widget.config.direction == Axis.horizontal
? draggingState.dropAreaSize.width
: draggingState.dropAreaSize.height / 2.0;
if (_scrollController.hasClients) {
final double scrollOffset = _scrollController.offset;
final double topOffset = max(

View File

@ -46,15 +46,23 @@ class BoardPhantomController extends OverlapDragTargetDelegate
required this.groupsState,
});
bool isFromGroup(String groupId) {
/// Determines whether the group should perform reorder
///
/// Returns `true` if the fromGroupId and toGroupId of the phantomRecord
/// equal to the passed in groupId.
///
/// Returns `true` if the phantomRecord is null
///
bool shouldReorder(String groupId) {
if (phantomRecord != null) {
return phantomRecord!.fromGroupId == groupId;
return phantomRecord!.toGroupId == groupId &&
phantomRecord!.fromGroupId == groupId;
} else {
return true;
}
}
void transformIndex(int fromIndex, int toIndex) {
void updateIndex(int fromIndex, int toIndex) {
if (phantomRecord == null) {
return;
}
@ -69,7 +77,6 @@ class BoardPhantomController extends OverlapDragTargetDelegate
/// Remove the phantom in the group when the group is end dragging.
void groupEndDragging(String groupId) {
phantomState.setGroupIsDragging(groupId, false);
if (phantomRecord == null) return;
final fromGroupId = phantomRecord!.fromGroupId;
@ -246,10 +253,6 @@ class PhantomRecord {
});
void updateFromGroupIndex(int index) {
if (fromGroupIndex == index) {
return;
}
fromGroupIndex = index;
}

View File

@ -1,6 +1,6 @@
name: appflowy_board
description: AppFlowyBoard is a board-style widget that consists of multi-groups. It supports drag and drop between different groups.
version: 0.0.8
version: 0.0.9
homepage: https://github.com/AppFlowy-IO/AppFlowy
repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board

View File

@ -1,3 +1,13 @@
## 0.0.6
* Add three plugins: Code Block, LateX, and Horizontal rule.
* Support web platform.
* Support more markdown syntax conversions.
* `~ ~` to format text as strikethrough
* `_ _` to format text as italic
* \` \` to format text as code
* `[]()` to format text as link
* Fix some bugs.
## 0.0.5
* Support customize the hotkeys for a shortcut on different platforms.
* Support customize a theme.

View File

@ -2,6 +2,8 @@ import 'dart:convert';
import 'dart:io';
import 'package:example/plugin/code_block_node_widget.dart';
import 'package:example/plugin/horizontal_rule_node_widget.dart';
import 'package:example/plugin/tex_block_node_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -119,14 +121,19 @@ class _MyHomePageState extends State<MyHomePage> {
editable: true,
customBuilders: {
'text/code_block': CodeBlockNodeWidgetBuilder(),
'tex': TeXBlockNodeWidgetBuidler(),
'horizontal_rule': HorizontalRuleWidgetBuilder(),
},
shortcutEvents: [
enterInCodeBlock,
ignoreKeysInCodeBlock,
underscoreToItalic,
insertHorizontalRule,
],
selectionMenuItems: [
codeBlockItem,
codeBlockMenuItem,
teXBlockMenuItem,
horizontalRuleMenuItem,
],
),
);

View File

@ -44,9 +44,13 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
return KeyEventResult.ignored;
};
SelectionMenuItem codeBlockItem = SelectionMenuItem(
name: 'Code Block',
icon: const Icon(Icons.abc),
SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
name: () => 'Code Block',
icon: const Icon(
Icons.abc,
color: Colors.black,
size: 18.0,
),
keywords: ['code block'],
handler: (editorState, _, __) {
final selection =

View File

@ -0,0 +1,167 @@
import 'dart:collection';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
ShortcutEvent insertHorizontalRule = ShortcutEvent(
key: 'Horizontal rule',
command: 'Minus',
handler: _insertHorzaontalRule,
);
ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
final selection = editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (textNodes.length != 1 || selection == null) {
return KeyEventResult.ignored;
}
final textNode = textNodes.first;
if (textNode.toRawString() == '--') {
TransactionBuilder(editorState)
..deleteText(textNode, 0, 2)
..insertNode(
textNode.path,
Node(
type: 'horizontal_rule',
children: LinkedList(),
attributes: {},
),
)
..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0)
..commit();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
name: () => 'Horizontal rule',
icon: const Icon(
Icons.horizontal_rule,
color: Colors.black,
size: 18.0,
),
keywords: ['horizontal rule'],
handler: (editorState, _, __) {
final selection =
editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (selection == null || textNodes.isEmpty) {
return;
}
final textNode = textNodes.first;
if (textNode.toRawString().isEmpty) {
TransactionBuilder(editorState)
..insertNode(
textNode.path,
Node(
type: 'horizontal_rule',
children: LinkedList(),
attributes: {},
),
)
..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0)
..commit();
} else {
TransactionBuilder(editorState)
..insertNode(
selection.end.path.next,
TextNode(
type: 'text',
children: LinkedList(),
attributes: {
'subtype': 'horizontal_rule',
},
delta: Delta()..insert('---'),
),
)
..afterSelection = selection
..commit();
}
},
);
class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
@override
Widget build(NodeWidgetContext<Node> context) {
return _HorizontalRuleWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
return true;
};
}
class _HorizontalRuleWidget extends StatefulWidget {
const _HorizontalRuleWidget({
Key? key,
required this.node,
required this.editorState,
}) : super(key: key);
final Node node;
final EditorState editorState;
@override
State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
}
class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
with SelectableMixin {
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Container(
height: 1,
color: Colors.grey,
),
);
}
@override
Position start() => Position(path: widget.node.path, offset: 0);
@override
Position end() => Position(path: widget.node.path, offset: 1);
@override
Position getPositionInOffset(Offset start) => end();
@override
bool get shouldCursorBlink => false;
@override
CursorStyle get cursorStyle => CursorStyle.borderLine;
@override
Rect? getCursorRectInPosition(Position position) {
final size = _renderBox.size;
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
}
@override
List<Rect> getRectsInSelection(Selection selection) =>
[Offset.zero & _renderBox.size];
@override
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
path: widget.node.path,
startOffset: 0,
endOffset: 1,
);
@override
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
}

View File

@ -0,0 +1,193 @@
import 'dart:collection';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_math_fork/flutter_math.dart';
SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
name: () => 'Tex',
icon: const Icon(
Icons.text_fields_rounded,
color: Colors.black,
size: 18.0,
),
keywords: ['tex, latex, katex'],
handler: (editorState, _, __) {
final selection =
editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (selection == null || !selection.isCollapsed || textNodes.isEmpty) {
return;
}
final Path texNodePath;
if (textNodes.first.toRawString().isEmpty) {
texNodePath = selection.end.path;
TransactionBuilder(editorState)
..insertNode(
selection.end.path,
Node(
type: 'tex',
children: LinkedList(),
attributes: {'tex': ''},
),
)
..deleteNode(textNodes.first)
..afterSelection = selection
..commit();
} else {
texNodePath = selection.end.path.next;
TransactionBuilder(editorState)
..insertNode(
selection.end.path.next,
Node(
type: 'tex',
children: LinkedList(),
attributes: {'tex': ''},
),
)
..afterSelection = selection
..commit();
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final texState =
editorState.document.nodeAtPath(texNodePath)?.key?.currentState;
if (texState != null && texState is __TeXBlockNodeWidgetState) {
texState.showEditingDialog();
}
});
},
);
class TeXBlockNodeWidgetBuidler extends NodeWidgetBuilder<Node> {
@override
Widget build(NodeWidgetContext<Node> context) {
return _TeXBlockNodeWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
return node.attributes['tex'] is String;
};
}
class _TeXBlockNodeWidget extends StatefulWidget {
const _TeXBlockNodeWidget({
Key? key,
required this.node,
required this.editorState,
}) : super(key: key);
final Node node;
final EditorState editorState;
@override
State<_TeXBlockNodeWidget> createState() => __TeXBlockNodeWidgetState();
}
class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
String get _tex => widget.node.attributes['tex'] as String;
bool _isHover = false;
@override
Widget build(BuildContext context) {
return InkWell(
onHover: (value) {
setState(() {
_isHover = value;
});
},
onTap: () {
showEditingDialog();
},
child: Stack(
children: [
_buildTex(context),
if (_isHover) _buildDeleteButton(context),
],
),
);
}
Widget _buildTex(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: _isHover ? Colors.grey[200] : Colors.transparent,
),
child: Center(
child: Math.tex(
_tex,
textStyle: const TextStyle(fontSize: 20),
mathStyle: MathStyle.display,
),
),
);
}
Widget _buildDeleteButton(BuildContext context) {
return Positioned(
top: -5,
right: -5,
child: IconButton(
icon: Icon(
Icons.delete_outline,
color: Colors.blue[400],
size: 16,
),
onPressed: () {
TransactionBuilder(widget.editorState)
..deleteNode(widget.node)
..commit();
},
),
);
}
void showEditingDialog() {
showDialog(
context: context,
builder: (context) {
final controller = TextEditingController(text: _tex);
return AlertDialog(
title: const Text('Edit Katex'),
content: TextField(
controller: controller,
maxLines: null,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
if (controller.text != _tex) {
TransactionBuilder(widget.editorState)
..updateNode(
widget.node,
{'tex': controller.text},
)
..commit();
}
},
child: const Text('OK'),
),
],
);
},
);
}
}

View File

@ -24,7 +24,7 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3

View File

@ -44,6 +44,7 @@ dependencies:
file_picker: ^5.0.1
universal_html: ^2.0.8
highlight: ^0.7.0
flutter_math_fork: ^0.6.3+1
dev_dependencies:
flutter_test:

View File

@ -0,0 +1,35 @@
{
"@@locale": "cs-CZ",
"bold": "Tučně",
"@bold": {},
"bulletedList": "Odrážkový seznam",
"@bulletedList": {},
"checkbox": "Zaškrtávací políčko",
"@checkbox": {},
"embedCode": "Vložit kód",
"@embedCode": {},
"heading1": "Nadpis 1",
"@heading1": {},
"heading2": "Nadpis 2",
"@heading2": {},
"heading3": "Nadpis 3",
"@heading3": {},
"highlight": "Zvýraznění",
"@highlight": {},
"image": "Obrázek",
"@image": {},
"italic": "Kurzíva",
"@italic": {},
"link": "Odkaz",
"@link": {},
"numberedList": "Číslovaný seznam",
"@numberedList": {},
"quote": "Citace",
"@quote": {},
"strikethrough": "Přeškrtnutí",
"@strikethrough": {},
"text": "Text",
"@text": {},
"underline": "Podtržení",
"@underline": {}
}

View File

@ -1,35 +1,35 @@
{
"@@locale": "fr-CA",
"bold": "",
"bold": "gras",
"@bold": {},
"bulletedList": "",
"bulletedList": "liste à puces",
"@bulletedList": {},
"checkbox": "",
"checkbox": "case à cocher",
"@checkbox": {},
"embedCode": "",
"embedCode": "incorporer Code",
"@embedCode": {},
"heading1": "",
"heading1": "en-tête1",
"@heading1": {},
"heading2": "",
"heading2": "en-tête2",
"@heading2": {},
"heading3": "",
"heading3": "en-tête3",
"@heading3": {},
"highlight": "",
"highlight": "mettre en évidence",
"@highlight": {},
"image": "",
"image": "limage",
"@image": {},
"italic": "",
"italic": "italique",
"@italic": {},
"link": "",
"link": "lien",
"@link": {},
"numberedList": "",
"numberedList": "liste numérotée",
"@numberedList": {},
"quote": "",
"quote": "citation",
"@quote": {},
"strikethrough": "",
"strikethrough": "barré",
"@strikethrough": {},
"text": "",
"text": "texte",
"@text": {},
"underline": "",
"underline": "souligner",
"@underline": {}
}

View File

@ -1,35 +1,35 @@
{
"@@locale": "fr-FR",
"bold": "",
"bold": "Gras",
"@bold": {},
"bulletedList": "",
"bulletedList": "List à puces",
"@bulletedList": {},
"checkbox": "",
"checkbox": "Case à cocher",
"@checkbox": {},
"embedCode": "",
"embedCode": "Incorporer code",
"@embedCode": {},
"heading1": "",
"heading1": "Titre 1",
"@heading1": {},
"heading2": "",
"heading2": "Titre 2",
"@heading2": {},
"heading3": "",
"heading3": "Titre 3",
"@heading3": {},
"highlight": "",
"highlight": "Surligné",
"@highlight": {},
"image": "",
"image": "Image",
"@image": {},
"italic": "",
"italic": "Italique",
"@italic": {},
"link": "",
"link": "Lien",
"@link": {},
"numberedList": "",
"numberedList": "Liste numérotée",
"@numberedList": {},
"quote": "",
"quote": "Citation",
"@quote": {},
"strikethrough": "",
"strikethrough": "Barré",
"@strikethrough": {},
"text": "",
"text": "Texte",
"@text": {},
"underline": "",
"underline": "Souligné",
"@underline": {}
}

View File

@ -1,35 +1,35 @@
{
"@@locale": "hu-HU",
"bold": "",
"bold": "bátor",
"@bold": {},
"bulletedList": "",
"bulletedList": "pontozott lista",
"@bulletedList": {},
"checkbox": "",
"checkbox": "jelölőnégyzetet",
"@checkbox": {},
"embedCode": "",
"embedCode": "Beágyazás",
"@embedCode": {},
"heading1": "",
"heading1": "címsor1",
"@heading1": {},
"heading2": "",
"heading2": "címsor2",
"@heading2": {},
"heading3": "",
"heading3": "címsor3",
"@heading3": {},
"highlight": "",
"highlight": "Kiemel",
"@highlight": {},
"image": "",
"image": "kép",
"@image": {},
"italic": "",
"italic": "dőlt",
"@italic": {},
"link": "",
"link": "link",
"@link": {},
"numberedList": "",
"numberedList": "számozottLista",
"@numberedList": {},
"quote": "",
"quote": "idézet",
"@quote": {},
"strikethrough": "",
"strikethrough": "áthúzott",
"@strikethrough": {},
"text": "",
"text": "szöveg",
"@text": {},
"underline": "",
"underline": "aláhúzás",
"@underline": {}
}

View File

@ -1,35 +1,35 @@
{
"@@locale": "id-ID",
"bold": "",
"bold": "berani",
"@bold": {},
"bulletedList": "",
"bulletedList": "daftar berpoin",
"@bulletedList": {},
"checkbox": "",
"checkbox": "kotak centang",
"@checkbox": {},
"embedCode": "",
"embedCode": "menyematkan Kode",
"@embedCode": {},
"heading1": "",
"heading1": "pos1",
"@heading1": {},
"heading2": "",
"heading2": "pos2",
"@heading2": {},
"heading3": "",
"heading3": "pos3",
"@heading3": {},
"highlight": "",
"highlight": "menyorot",
"@highlight": {},
"image": "",
"image": "gambar",
"@image": {},
"italic": "",
"italic": "miring",
"@italic": {},
"link": "",
"link": "tautan",
"@link": {},
"numberedList": "",
"numberedList": "daftar bernomor",
"@numberedList": {},
"quote": "",
"quote": "mengutip",
"@quote": {},
"strikethrough": "",
"strikethrough": "coret",
"@strikethrough": {},
"text": "",
"text": "teks",
"@text": {},
"underline": "",
"underline": "menggarisbawahi",
"@underline": {}
}

View File

@ -1,35 +1,35 @@
{
"@@locale": "it-IT",
"bold": "",
"bold": "Grassetto",
"@bold": {},
"bulletedList": "",
"bulletedList": "Elenco puntato",
"@bulletedList": {},
"checkbox": "",
"checkbox": "Casella di spunta",
"@checkbox": {},
"embedCode": "",
"embedCode": "Incorpora codice",
"@embedCode": {},
"heading1": "",
"heading1": "H1",
"@heading1": {},
"heading2": "",
"heading2": "H2",
"@heading2": {},
"heading3": "",
"heading3": "H3",
"@heading3": {},
"highlight": "",
"highlight": "Evidenzia",
"@highlight": {},
"image": "",
"image": "Immagine",
"@image": {},
"italic": "",
"italic": "Corsivo",
"@italic": {},
"link": "",
"link": "Collegamento",
"@link": {},
"numberedList": "",
"numberedList": "Elenco numerato",
"@numberedList": {},
"quote": "",
"quote": "Cita",
"@quote": {},
"strikethrough": "",
"strikethrough": "Barrato",
"@strikethrough": {},
"text": "",
"text": "Testo",
"@text": {},
"underline": "",
"underline": "Sottolineato",
"@underline": {}
}

View File

@ -0,0 +1,35 @@
{
"@@locale": "ml_IN",
"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

@ -1,35 +1,35 @@
{
"@@locale": "pt-PT",
"bold": "",
"bold": "negrito",
"@bold": {},
"bulletedList": "",
"bulletedList": "lista com marcadores",
"@bulletedList": {},
"checkbox": "",
"checkbox": "caixa de seleção",
"@checkbox": {},
"embedCode": "",
"embedCode": "Código embutido",
"@embedCode": {},
"heading1": "",
"heading1": "Cabeçallho 1",
"@heading1": {},
"heading2": "",
"heading2": "Cabeçallho 2",
"@heading2": {},
"heading3": "",
"heading3": "Cabeçallho 3",
"@heading3": {},
"highlight": "",
"highlight": "realçar",
"@highlight": {},
"image": "",
"image": "imagem",
"@image": {},
"italic": "",
"italic": "itálico",
"@italic": {},
"link": "",
"link": "link",
"@link": {},
"numberedList": "",
"numberedList": "lista numerada",
"@numberedList": {},
"quote": "",
"quote": "citar",
"@quote": {},
"strikethrough": "",
"strikethrough": "tachado",
"@strikethrough": {},
"text": "",
"text": "texto",
"@text": {},
"underline": "",
"underline": "sublinhado",
"@underline": {}
}

View File

@ -17,9 +17,9 @@ extension NodeAttributesExtensions on Attributes {
return containsKey(BuiltInAttributeKey.quote);
}
int? get number {
num? get number {
if (containsKey(BuiltInAttributeKey.number) &&
this[BuiltInAttributeKey.number] is int) {
this[BuiltInAttributeKey.number] is num) {
return this[BuiltInAttributeKey.number];
}
return null;
@ -27,7 +27,7 @@ extension NodeAttributesExtensions on Attributes {
bool get code {
if (containsKey(BuiltInAttributeKey.code) &&
this[BuiltInAttributeKey.code] == true) {
this[BuiltInAttributeKey.code] is bool) {
return this[BuiltInAttributeKey.code];
}
return false;
@ -63,11 +63,14 @@ extension DeltaAttributesExtensions on Attributes {
this[BuiltInAttributeKey.strikethrough] == true);
}
static const whiteInt = 0XFFFFFFFF;
Color? get color {
if (containsKey(BuiltInAttributeKey.color) &&
this[BuiltInAttributeKey.color] is String) {
return Color(
int.parse(this[BuiltInAttributeKey.color]),
// If the parse fails returns white by default
int.tryParse(this[BuiltInAttributeKey.color]) ?? whiteInt,
);
}
return null;
@ -77,8 +80,7 @@ extension DeltaAttributesExtensions on Attributes {
if (containsKey(BuiltInAttributeKey.backgroundColor) &&
this[BuiltInAttributeKey.backgroundColor] is String) {
return Color(
int.parse(this[BuiltInAttributeKey.backgroundColor]),
);
int.tryParse(this[BuiltInAttributeKey.backgroundColor]) ?? whiteInt);
}
return null;
}

View File

@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/document/text_delta.dart';
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
extension TextNodeExtension on TextNode {
dynamic getAttributeInSelection(Selection selection, String styleKey) {
T? getAttributeInSelection<T>(Selection selection, String styleKey) {
final ops = delta.whereType<TextInsert>();
final startOffset =
selection.isBackward ? selection.start.offset : selection.end.offset;
@ -19,8 +19,9 @@ extension TextNodeExtension on TextNode {
}
final length = op.length;
if (start < endOffset && start + length > startOffset) {
if (op.attributes?.containsKey(styleKey) == true) {
return op.attributes![styleKey];
final attributes = op.attributes;
if (attributes != null && attributes[styleKey] is T?) {
return attributes[styleKey];
}
}
start += length;

View File

@ -16,6 +16,7 @@ import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_ca.dart' as messages_ca;
import 'messages_cs-CZ.dart' as messages_cs_cz;
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;
@ -25,6 +26,8 @@ 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_ml_IN.dart' as messages_ml_in;
import 'messages_nl-NL.dart' as messages_nl_nl;
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;
@ -36,6 +39,7 @@ import 'messages_zh-TW.dart' as messages_zh_tw;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'ca': () => new Future.value(null),
'cs_CZ': () => new Future.value(null),
'de_DE': () => new Future.value(null),
'en': () => new Future.value(null),
'es_VE': () => new Future.value(null),
@ -45,6 +49,8 @@ Map<String, LibraryLoader> _deferredLibraries = {
'id_ID': () => new Future.value(null),
'it_IT': () => new Future.value(null),
'ja_JP': () => new Future.value(null),
'ml_IN': () => new Future.value(null),
'nl_NL': () => new Future.value(null),
'pl_PL': () => new Future.value(null),
'pt_BR': () => new Future.value(null),
'pt_PT': () => new Future.value(null),
@ -58,6 +64,8 @@ MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'ca':
return messages_ca.messages;
case 'cs_CZ':
return messages_cs_cz.messages;
case 'de_DE':
return messages_de_de.messages;
case 'en':
@ -76,6 +84,10 @@ MessageLookupByLibrary? _findExact(String localeName) {
return messages_it_it.messages;
case 'ja_JP':
return messages_ja_jp.messages;
case 'ml_IN':
return messages_ml_in.messages;
case 'nl_NL':
return messages_nl_nl.messages;
case 'pl_PL':
return messages_pl_pl.messages;
case 'pt_BR':

View File

@ -0,0 +1,44 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a cs_CZ 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 => 'cs_CZ';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage("Tučně"),
"bulletedList":
MessageLookupByLibrary.simpleMessage("Odrážkový seznam"),
"checkbox": MessageLookupByLibrary.simpleMessage("Zaškrtávací políčko"),
"embedCode": MessageLookupByLibrary.simpleMessage("Vložit kód"),
"heading1": MessageLookupByLibrary.simpleMessage("Nadpis 1"),
"heading2": MessageLookupByLibrary.simpleMessage("Nadpis 2"),
"heading3": MessageLookupByLibrary.simpleMessage("Nadpis 3"),
"highlight": MessageLookupByLibrary.simpleMessage("Zvýraznění"),
"image": MessageLookupByLibrary.simpleMessage("Obrázek"),
"italic": MessageLookupByLibrary.simpleMessage("Kurzíva"),
"link": MessageLookupByLibrary.simpleMessage("Odkaz"),
"numberedList":
MessageLookupByLibrary.simpleMessage("Číslovaný seznam"),
"quote": MessageLookupByLibrary.simpleMessage("Citace"),
"strikethrough": MessageLookupByLibrary.simpleMessage("Přeškrtnutí"),
"text": MessageLookupByLibrary.simpleMessage("Text"),
"underline": MessageLookupByLibrary.simpleMessage("Podtržení")
};
}

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
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("")
"bold": MessageLookupByLibrary.simpleMessage("gras"),
"bulletedList": MessageLookupByLibrary.simpleMessage("liste à puces"),
"checkbox": MessageLookupByLibrary.simpleMessage("case à cocher"),
"embedCode": MessageLookupByLibrary.simpleMessage("incorporer Code"),
"heading1": MessageLookupByLibrary.simpleMessage("en-tête1"),
"heading2": MessageLookupByLibrary.simpleMessage("en-tête2"),
"heading3": MessageLookupByLibrary.simpleMessage("en-tête3"),
"highlight": MessageLookupByLibrary.simpleMessage("mettre en évidence"),
"image": MessageLookupByLibrary.simpleMessage("limage"),
"italic": MessageLookupByLibrary.simpleMessage("italique"),
"link": MessageLookupByLibrary.simpleMessage("lien"),
"numberedList": MessageLookupByLibrary.simpleMessage("liste numérotée"),
"quote": MessageLookupByLibrary.simpleMessage("citation"),
"strikethrough": MessageLookupByLibrary.simpleMessage("barré"),
"text": MessageLookupByLibrary.simpleMessage("texte"),
"underline": MessageLookupByLibrary.simpleMessage("souligner")
};
}

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
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("")
"bold": MessageLookupByLibrary.simpleMessage("Gras"),
"bulletedList": MessageLookupByLibrary.simpleMessage("List à puces"),
"checkbox": MessageLookupByLibrary.simpleMessage("Case à cocher"),
"embedCode": MessageLookupByLibrary.simpleMessage("Incorporer code"),
"heading1": MessageLookupByLibrary.simpleMessage("Titre 1"),
"heading2": MessageLookupByLibrary.simpleMessage("Titre 2"),
"heading3": MessageLookupByLibrary.simpleMessage("Titre 3"),
"highlight": MessageLookupByLibrary.simpleMessage("Surligné"),
"image": MessageLookupByLibrary.simpleMessage("Image"),
"italic": MessageLookupByLibrary.simpleMessage("Italique"),
"link": MessageLookupByLibrary.simpleMessage("Lien"),
"numberedList": MessageLookupByLibrary.simpleMessage("Liste numérotée"),
"quote": MessageLookupByLibrary.simpleMessage("Citation"),
"strikethrough": MessageLookupByLibrary.simpleMessage("Barré"),
"text": MessageLookupByLibrary.simpleMessage("Texte"),
"underline": MessageLookupByLibrary.simpleMessage("Souligné")
};
}

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
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("")
"bold": MessageLookupByLibrary.simpleMessage("bátor"),
"bulletedList": MessageLookupByLibrary.simpleMessage("pontozott lista"),
"checkbox": MessageLookupByLibrary.simpleMessage("jelölőnégyzetet"),
"embedCode": MessageLookupByLibrary.simpleMessage("Beágyazás"),
"heading1": MessageLookupByLibrary.simpleMessage("címsor1"),
"heading2": MessageLookupByLibrary.simpleMessage("címsor2"),
"heading3": MessageLookupByLibrary.simpleMessage("címsor3"),
"highlight": MessageLookupByLibrary.simpleMessage("Kiemel"),
"image": MessageLookupByLibrary.simpleMessage("kép"),
"italic": MessageLookupByLibrary.simpleMessage("dőlt"),
"link": MessageLookupByLibrary.simpleMessage("link"),
"numberedList": MessageLookupByLibrary.simpleMessage("számozottLista"),
"quote": MessageLookupByLibrary.simpleMessage("idézet"),
"strikethrough": MessageLookupByLibrary.simpleMessage("áthúzott"),
"text": MessageLookupByLibrary.simpleMessage("szöveg"),
"underline": MessageLookupByLibrary.simpleMessage("aláhúzás")
};
}

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
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("")
"bold": MessageLookupByLibrary.simpleMessage("berani"),
"bulletedList": MessageLookupByLibrary.simpleMessage("daftar berpoin"),
"checkbox": MessageLookupByLibrary.simpleMessage("kotak centang"),
"embedCode": MessageLookupByLibrary.simpleMessage("menyematkan Kode"),
"heading1": MessageLookupByLibrary.simpleMessage("pos1"),
"heading2": MessageLookupByLibrary.simpleMessage("pos2"),
"heading3": MessageLookupByLibrary.simpleMessage("pos3"),
"highlight": MessageLookupByLibrary.simpleMessage("menyorot"),
"image": MessageLookupByLibrary.simpleMessage("gambar"),
"italic": MessageLookupByLibrary.simpleMessage("miring"),
"link": MessageLookupByLibrary.simpleMessage("tautan"),
"numberedList": MessageLookupByLibrary.simpleMessage("daftar bernomor"),
"quote": MessageLookupByLibrary.simpleMessage("mengutip"),
"strikethrough": MessageLookupByLibrary.simpleMessage("coret"),
"text": MessageLookupByLibrary.simpleMessage("teks"),
"underline": MessageLookupByLibrary.simpleMessage("menggarisbawahi")
};
}

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
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("")
"bold": MessageLookupByLibrary.simpleMessage("Grassetto"),
"bulletedList": MessageLookupByLibrary.simpleMessage("Elenco puntato"),
"checkbox": MessageLookupByLibrary.simpleMessage("Casella di spunta"),
"embedCode": MessageLookupByLibrary.simpleMessage("Incorpora codice"),
"heading1": MessageLookupByLibrary.simpleMessage("H1"),
"heading2": MessageLookupByLibrary.simpleMessage("H2"),
"heading3": MessageLookupByLibrary.simpleMessage("H3"),
"highlight": MessageLookupByLibrary.simpleMessage("Evidenzia"),
"image": MessageLookupByLibrary.simpleMessage("Immagine"),
"italic": MessageLookupByLibrary.simpleMessage("Corsivo"),
"link": MessageLookupByLibrary.simpleMessage("Collegamento"),
"numberedList": MessageLookupByLibrary.simpleMessage("Elenco numerato"),
"quote": MessageLookupByLibrary.simpleMessage("Cita"),
"strikethrough": MessageLookupByLibrary.simpleMessage("Barrato"),
"text": MessageLookupByLibrary.simpleMessage("Testo"),
"underline": MessageLookupByLibrary.simpleMessage("Sottolineato")
};
}

View File

@ -0,0 +1,45 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ml_IN 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 => 'ml_IN';
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,43 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a nl_NL 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 => 'nl_NL';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage("Vet"),
"bulletedList":
MessageLookupByLibrary.simpleMessage("Opsommingstekens"),
"checkbox": MessageLookupByLibrary.simpleMessage("Selectievakje"),
"embedCode": MessageLookupByLibrary.simpleMessage("Invoegcode"),
"heading1": MessageLookupByLibrary.simpleMessage("H1"),
"heading2": MessageLookupByLibrary.simpleMessage("H2"),
"heading3": MessageLookupByLibrary.simpleMessage("H3"),
"highlight": MessageLookupByLibrary.simpleMessage("Highlight"),
"image": MessageLookupByLibrary.simpleMessage("Afbeelding"),
"italic": MessageLookupByLibrary.simpleMessage("Cursief"),
"link": MessageLookupByLibrary.simpleMessage(""),
"numberedList": MessageLookupByLibrary.simpleMessage("Nummering"),
"quote": MessageLookupByLibrary.simpleMessage("Quote"),
"strikethrough": MessageLookupByLibrary.simpleMessage("Doorhalen"),
"text": MessageLookupByLibrary.simpleMessage("Tekst"),
"underline": MessageLookupByLibrary.simpleMessage("Onderstrepen")
};
}

View File

@ -22,21 +22,22 @@ class MessageLookup extends MessageLookupByLibrary {
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("")
"bold": MessageLookupByLibrary.simpleMessage("Negrito"),
"bulletedList":
MessageLookupByLibrary.simpleMessage("Lista de marcadores"),
"checkbox": MessageLookupByLibrary.simpleMessage("Caixa de seleção"),
"embedCode": MessageLookupByLibrary.simpleMessage("Código incorporado"),
"heading1": MessageLookupByLibrary.simpleMessage("H1"),
"heading2": MessageLookupByLibrary.simpleMessage("H2"),
"heading3": MessageLookupByLibrary.simpleMessage("H3"),
"highlight": MessageLookupByLibrary.simpleMessage("Destacar"),
"image": MessageLookupByLibrary.simpleMessage("Imagem"),
"italic": MessageLookupByLibrary.simpleMessage("Itálico"),
"link": MessageLookupByLibrary.simpleMessage("Link"),
"numberedList": MessageLookupByLibrary.simpleMessage("Lista numerada"),
"quote": MessageLookupByLibrary.simpleMessage("Citar"),
"strikethrough": MessageLookupByLibrary.simpleMessage("Rasurar"),
"text": MessageLookupByLibrary.simpleMessage("Texto"),
"underline": MessageLookupByLibrary.simpleMessage("Sublinhar")
};
}

View File

@ -22,21 +22,22 @@ class MessageLookup extends MessageLookupByLibrary {
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("")
"bold": MessageLookupByLibrary.simpleMessage("negrito"),
"bulletedList":
MessageLookupByLibrary.simpleMessage("lista com marcadores"),
"checkbox": MessageLookupByLibrary.simpleMessage("caixa de seleção"),
"embedCode": MessageLookupByLibrary.simpleMessage("Código embutido"),
"heading1": MessageLookupByLibrary.simpleMessage("Cabeçallho 1"),
"heading2": MessageLookupByLibrary.simpleMessage("Cabeçallho 2"),
"heading3": MessageLookupByLibrary.simpleMessage("Cabeçallho 3"),
"highlight": MessageLookupByLibrary.simpleMessage("realçar"),
"image": MessageLookupByLibrary.simpleMessage("imagem"),
"italic": MessageLookupByLibrary.simpleMessage("itálico"),
"link": MessageLookupByLibrary.simpleMessage("link"),
"numberedList": MessageLookupByLibrary.simpleMessage("lista numerada"),
"quote": MessageLookupByLibrary.simpleMessage("citar"),
"strikethrough": MessageLookupByLibrary.simpleMessage("tachado"),
"text": MessageLookupByLibrary.simpleMessage("texto"),
"underline": MessageLookupByLibrary.simpleMessage("sublinhado")
};
}

View File

@ -220,6 +220,7 @@ class AppLocalizationDelegate
return const <Locale>[
Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'ca'),
Locale.fromSubtags(languageCode: 'cs', countryCode: 'CZ'),
Locale.fromSubtags(languageCode: 'de', countryCode: 'DE'),
Locale.fromSubtags(languageCode: 'es', countryCode: 'VE'),
Locale.fromSubtags(languageCode: 'fr', countryCode: 'CA'),
@ -228,6 +229,8 @@ class AppLocalizationDelegate
Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'),
Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'),
Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
Locale.fromSubtags(languageCode: 'ml', countryCode: 'IN'),
Locale.fromSubtags(languageCode: 'nl', countryCode: 'NL'),
Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'),
Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'),
Locale.fromSubtags(languageCode: 'pt', countryCode: 'PT'),

View File

@ -1,5 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:flutter/material.dart';
abstract class BuiltInTextWidget extends StatefulWidget {

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:flutter/material.dart';
class CursorWidget extends StatefulWidget {
@ -9,9 +10,13 @@ class CursorWidget extends StatefulWidget {
required this.rect,
required this.color,
this.blinkingInterval = 0.5,
this.shouldBlink = true,
this.cursorStyle = CursorStyle.verticalLine,
}) : super(key: key);
final double blinkingInterval; // milliseconds
final bool shouldBlink;
final CursorStyle cursorStyle;
final Color color;
final Rect rect;
final LayerLink layerLink;
@ -67,11 +72,28 @@ class CursorWidgetState extends State<CursorWidget> {
// Ignore the gestures in cursor
// to solve the problem that cursor area cannot be selected.
child: IgnorePointer(
child: Container(
color: showCursor ? widget.color : Colors.transparent,
),
child: _buildCursor(context),
),
),
);
}
Widget _buildCursor(BuildContext context) {
var color = widget.color;
if (widget.shouldBlink && !showCursor) {
color = Colors.transparent;
}
switch (widget.cursorStyle) {
case CursorStyle.verticalLine:
return Container(
color: color,
);
case CursorStyle.borderLine:
return Container(
decoration: BoxDecoration(
border: Border.all(color: color, width: 2),
),
);
}
}
}

View File

@ -2,6 +2,11 @@ import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart';
import 'package:flutter/material.dart';
enum CursorStyle {
verticalLine,
borderLine,
}
/// [SelectableMixin] is used for the editor to calculate the position
/// and size of the selection.
///
@ -53,4 +58,8 @@ mixin SelectableMixin<T extends StatefulWidget> on State<T> {
Selection? getWorldBoundaryInOffset(Offset start) {
return null;
}
bool get shouldCursorBlink => true;
CursorStyle get cursorStyle => CursorStyle.verticalLine;
}

View File

@ -169,6 +169,14 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
insertBulletedListAfterSelection(editorState);
},
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.numberedList,
icon: _selectionMenuIcon('number'),
keywords: ['numbered list', 'list', 'ordered list'],
handler: (editorState, _, __) {
insertNumberedListAfterSelection(editorState);
},
),
SelectionMenuItem(
name: () => AppFlowyEditorLocalizations.current.checkbox,
icon: _selectionMenuIcon('checkbox'),

View File

@ -7,10 +7,10 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
typedef SelectionMenuItemHandler = void Function(
EditorState editorState,
SelectionMenuService menuService,
BuildContext context,
);
EditorState editorState,
SelectionMenuService menuService,
BuildContext context,
);
/// Selection Menu Item
class SelectionMenuItem {
@ -23,7 +23,7 @@ class SelectionMenuItem {
this.handler = (editorState, menuService, context) {
_deleteToSlash(editorState);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
handler(editorState, menuService, context);
handler(editorState, menuService, context);
});
};
}

View File

@ -333,8 +333,10 @@ void showLinkMenu(
final textNode = node.first as TextNode;
String? linkText;
if (textNode.allSatisfyLinkInSelection(selection)) {
linkText =
textNode.getAttributeInSelection(selection, BuiltInAttributeKey.href);
linkText = textNode.getAttributeInSelection<String>(
selection,
BuiltInAttributeKey.href,
);
}
_linkMenuOverlay = OverlayEntry(builder: (context) {
return Positioned(

View File

@ -34,6 +34,13 @@ void insertBulletedListAfterSelection(EditorState editorState) {
});
}
void insertNumberedListAfterSelection(EditorState editorState) {
insertTextNodeAfterSelection(editorState, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
BuiltInAttributeKey.number: 1,
});
}
bool insertTextNodeAfterSelection(
EditorState editorState, Attributes attributes) {
final selection = editorState.service.selectionService.currentSelection.value;

View File

@ -3,7 +3,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_l
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
// Handle delete text.
ShortcutEventHandler deleteTextHandler = (editorState, event) {
@ -84,6 +83,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
}
} else {
if (textNodes.isEmpty) {
if (nonTextNodes.isNotEmpty) {
transactionBuilder.afterSelection =
Selection.collapsed(selection.start);
}
transactionBuilder.commit();
return KeyEventResult.handled;
}
final startPosition = selection.start;

View File

@ -1,7 +1,6 @@
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/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';

View File

@ -3,7 +3,6 @@ import 'dart:collection';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
import './number_list_helper.dart';
/// Handle some cases where enter is pressed and shift is not pressed.

View File

@ -1,4 +1,3 @@
import "dart:math";
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
@ -49,7 +48,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
.substring(selection.start.offset, selection.end.offset);
// toggle code style when selected some text
if (selectionText.length > 0) {
if (selectionText.isNotEmpty) {
formatEmbedCode(editorState);
return KeyEventResult.handled;
}
@ -124,3 +123,121 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
return KeyEventResult.handled;
};
// convert ~~abc~~ to strikethrough abc.
ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
if (selection == null || !selection.isSingle || textNodes.length != 1) {
return KeyEventResult.ignored;
}
final textNode = textNodes.first;
final text = textNode.toRawString().substring(0, selection.end.offset);
// make sure the last two characters are ~~.
if (text.length < 2 || text[selection.end.offset - 1] != '~') {
return KeyEventResult.ignored;
}
// find all the index of `~`.
final tildeIndexes = <int>[];
for (var i = 0; i < text.length; i++) {
if (text[i] == '~') {
tildeIndexes.add(i);
}
}
if (tildeIndexes.length < 3) {
return KeyEventResult.ignored;
}
// make sure the second to last and third to last tildes are connected.
final thirdToLastTildeIndex = tildeIndexes[tildeIndexes.length - 3];
final secondToLastTildeIndex = tildeIndexes[tildeIndexes.length - 2];
final lastTildeIndex = tildeIndexes[tildeIndexes.length - 1];
if (secondToLastTildeIndex != thirdToLastTildeIndex + 1 ||
lastTildeIndex == secondToLastTildeIndex + 1) {
return KeyEventResult.ignored;
}
// delete the last three tildes.
// update the style of the text surround by `~~ ~~` to strikethrough.
// and update the cursor position.
TransactionBuilder(editorState)
..deleteText(textNode, lastTildeIndex, 1)
..deleteText(textNode, thirdToLastTildeIndex, 2)
..formatText(
textNode,
thirdToLastTildeIndex,
selection.end.offset - thirdToLastTildeIndex - 2,
{
BuiltInAttributeKey.strikethrough: true,
},
)
..afterSelection = Selection.collapsed(
Position(
path: textNode.path,
offset: selection.end.offset - 3,
),
)
..commit();
return KeyEventResult.handled;
};
/// To create a link, enclose the link text in brackets (e.g., [link text]).
/// Then, immediately follow it with the URL in parentheses (e.g., (https://example.com)).
ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
if (selection == null || !selection.isSingle || textNodes.length != 1) {
return KeyEventResult.ignored;
}
// find all of the indexs for important characters
final textNode = textNodes.first;
final text = textNode.toRawString();
final firstOpeningBracket = text.indexOf('[');
final firstClosingBracket = text.indexOf(']');
// use regex to validate the format of the link
// note: this enforces that the link has http or https
final regexp = RegExp(r'\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d./?=#]+)$');
final match = regexp.firstMatch(text);
if (match == null) {
return KeyEventResult.ignored;
}
// extract the text and the url of the link
final linkText = match.group(1);
final linkUrl = match.group(2);
// Delete the initial opening bracket,
// update the href attribute of the text surrounded by [ ] to the url,
// delete everything after the text,
// and update the cursor position.
TransactionBuilder(editorState)
..deleteText(textNode, firstOpeningBracket, 1)
..formatText(
textNode,
firstOpeningBracket,
firstClosingBracket - firstOpeningBracket - 1,
{
BuiltInAttributeKey.href: linkUrl,
},
)
..deleteText(textNode, firstClosingBracket - 1,
selection.end.offset - firstClosingBracket)
..afterSelection = Selection.collapsed(
Position(
path: textNode.path,
offset: firstOpeningBracket + linkText!.length,
),
)
..commit();
return KeyEventResult.handled;
};

View File

@ -457,6 +457,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
rect: cursorRect,
color: widget.cursorColor,
layerLink: node.layerLink,
shouldBlink: selectable.shouldCursorBlink,
cursorStyle: selectable.cursorStyle,
),
);

View File

@ -263,10 +263,21 @@ List<ShortcutEvent> builtInShortcutEvents = [
command: 'shift+underscore',
handler: doubleUnderscoresToBold,
),
ShortcutEvent(
key: 'Backquote to code',
command: 'backquote',
handler: backquoteToCodeHandler,
),
ShortcutEvent(
key: 'Double tilde to strikethrough',
command: 'shift+tilde',
handler: doubleTildeToStrikethrough,
),
ShortcutEvent(
key: 'Markdown link to link',
command: 'shift+parenthesis right',
handler: markdownLinkToLinkHandler,
),
// https://github.com/flutter/flutter/issues/104944
// Workaround: Using space editing on the web platform often results in errors,
// so adding a shortcut event to handle the space input instead of using the

View File

@ -1,12 +1,13 @@
name: appflowy_editor
description: A highly customizable rich-text editor for Flutter
version: 0.0.5
version: 0.0.6
homepage: https://github.com/AppFlowy-IO/AppFlowy
platforms:
linux:
macos:
windows:
web:
environment:
sdk: ">=2.17.0 <3.0.0"
@ -32,6 +33,7 @@ dev_dependencies:
sdk: flutter
flutter_lints: ^2.0.1
network_image_mock: ^2.1.1
mockito: ^5.3.2
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -0,0 +1,201 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('NodeAttributesExtensions::', () {
test('heading', () {
final Attributes attribute = {
'subtype': 'heading',
'heading': 'AppFlowy',
};
expect(attribute.heading, 'AppFlowy');
});
test('heading - text is not String return null', () {
final Attributes attribute = {
'subtype': 'heading',
'heading': 123,
};
expect(attribute.heading, null);
});
test('heading - subtype is not "heading" return null', () {
final Attributes attribute = {
'subtype': 'code',
'heading': 'Hello World!',
};
expect(attribute.heading, null);
});
test('quote', () {
final Attributes attribute = {
'quote': 'quote text',
};
expect(attribute.quote, true);
});
test('number - int', () {
final Attributes attribute = {
'number': 99,
};
expect(attribute.number, 99);
});
test('number - double', () {
final Attributes attribute = {
'number': 12.34,
};
expect(attribute.number, 12.34);
});
test('number - return null', () {
final Attributes attribute = {
'code': 12.34,
};
expect(attribute.number, null);
});
test('code', () {
final Attributes attribute = {
'code': true,
};
expect(attribute.code, true);
});
test('code - return false', () {
final Attributes attribute = {
'quote': true,
};
expect(attribute.code, false);
});
test('check', () {
final Attributes attribute = {
'checkbox': true,
};
expect(attribute.check, true);
});
test('check - return false', () {
final Attributes attribute = {
'quote': true,
};
expect(attribute.check, false);
});
});
group('DeltaAttributesExtensions::', () {
test('bold', () {
final Attributes attribute = {
'bold': true,
};
expect(attribute.bold, true);
});
test('bold - return false', () {
final Attributes attribute = {
'bold': 123,
};
expect(attribute.bold, false);
});
test('italic', () {
final Attributes attribute = {
'italic': true,
};
expect(attribute.italic, true);
});
test('italic - return false', () {
final Attributes attribute = {
'italic': 123,
};
expect(attribute.italic, false);
});
test('underline', () {
final Attributes attribute = {
'underline': true,
};
expect(attribute.underline, true);
});
test('underline - return false', () {
final Attributes attribute = {
'underline': 123,
};
expect(attribute.underline, false);
});
test('strikethrough', () {
final Attributes attribute = {
'strikethrough': true,
};
expect(attribute.strikethrough, true);
});
test('strikethrough - return false', () {
final Attributes attribute = {
'strikethrough': 123,
};
expect(attribute.strikethrough, false);
});
test('color', () {
final Attributes attribute = {
'color': '0xff212fff',
};
expect(attribute.color, const Color(0XFF212FFF));
});
test('color - return null', () {
final Attributes attribute = {
'color': 123,
};
expect(attribute.color, null);
});
test('color - parse failure return white', () {
final Attributes attribute = {
'color': 'hello123',
};
expect(attribute.color, const Color(0XFFFFFFFF));
});
test('backgroundColor', () {
final Attributes attribute = {
'backgroundColor': '0xff678fff',
};
expect(attribute.backgroundColor, const Color(0XFF678FFF));
});
test('backgroundColor - return null', () {
final Attributes attribute = {
'backgroundColor': 123,
};
expect(attribute.backgroundColor, null);
});
test('backgroundColor - parse failure return white', () {
final Attributes attribute = {
'backgroundColor': 'hello123',
};
expect(attribute.backgroundColor, const Color(0XFFFFFFFF));
});
test('href', () {
final Attributes attribute = {
'href': '/app/flowy',
};
expect(attribute.href, '/app/flowy');
});
test('href - return null', () {
final Attributes attribute = {
'href': 123,
};
expect(attribute.href, null);
});
});
}

View File

@ -0,0 +1,40 @@
import 'package:appflowy_editor/src/extensions/color_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ColorExtension::', () {
const white = Color(0XFFFFFFFF);
const black = Color(0XFF000000);
const blue = Color(0XFF000FFF);
const blueRgba = 'rgba(0, 15, 255, 255)';
test('ToRgbaString', () {
expect(blue.toRgbaString(), 'rgba(0, 15, 255, 255)');
expect(white.toRgbaString(), 'rgba(255, 255, 255, 255)');
expect(black.toRgbaString(), 'rgba(0, 0, 0, 255)');
});
test('tryFromRgbaString', () {
final color = ColorExtension.tryFromRgbaString(blueRgba);
expect(color, const Color.fromARGB(255, 0, 15, 255));
});
test('tryFromRgbaString - wrong rgba format return null', () {
const wrongRgba = 'abc(1,2,3,4)';
final color = ColorExtension.tryFromRgbaString(wrongRgba);
expect(color, null);
});
test('tryFromRgbaString - wrong length return null', () {
const wrongRgba = 'rgba(0, 15, 255)';
final color = ColorExtension.tryFromRgbaString(wrongRgba);
expect(color, null);
});
test('tryFromRgbaString - wrong values return null', () {
const wrongRgba = 'rgba(-12, 999, 1234, 619)';
final color = ColorExtension.tryFromRgbaString(wrongRgba);
expect(color, null);
});
});
}

View File

@ -0,0 +1,57 @@
import 'dart:collection';
import 'dart:ui';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
class MockNode extends Mock implements Node {}
void main() {
final mockNode = MockNode();
group('NodeExtensions::', () {
final selection = Selection(
start: Position(path: [0]),
end: Position(path: [1]),
);
test('rect - renderBox is null', () {
when(mockNode.renderBox).thenReturn(null);
final result = mockNode.rect;
expect(result, Rect.zero);
});
test('inSelection', () {
// I use an empty implementation instead of mock, because the mocked
// version throws error trying to access the path.
final subLinkedList = LinkedList<Node>()
..addAll([
Node(type: 'type', children: LinkedList(), attributes: {}),
Node(type: 'type', children: LinkedList(), attributes: {}),
Node(type: 'type', children: LinkedList(), attributes: {}),
Node(type: 'type', children: LinkedList(), attributes: {}),
Node(type: 'type', children: LinkedList(), attributes: {}),
]);
final linkedList = LinkedList<Node>()
..addAll([
Node(
type: 'type',
children: subLinkedList,
attributes: {},
),
]);
final node = Node(
type: 'type',
children: linkedList,
attributes: {},
);
final result = node.inSelection(selection);
expect(result, false);
});
});
}

View File

@ -0,0 +1,20 @@
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/src/extensions/object_extensions.dart';
void main() {
group('FlowyObjectExtensions::', () {
test('unwrapOrNull', () {
final result = const TextSpan().unwrapOrNull<HitTestTarget>();
assert(result is TextSpan);
});
test('unwrapOrNull - return null', () {
final result = const TextSpan().unwrapOrNull<ServerSocket>();
expect(result, null);
});
});
}

View File

@ -1,6 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
void main() async {
setUpAll(() {

View File

@ -0,0 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
group('TextNodeExtension::', () {
test('description', () {});
});
}

View File

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
void main() {
group('TextStyleExtensions::', () {
const style = TextStyle(
color: Colors.blue,
backgroundColor: Colors.white,
fontSize: 14,
height: 100,
wordSpacing: 2,
fontWeight: FontWeight.w700,
);
const otherStyle = TextStyle(
color: Colors.red,
backgroundColor: Colors.black,
fontSize: 12,
height: 10,
wordSpacing: 1,
);
test('combine', () {
final result = style.combine(otherStyle);
expect(result.color, Colors.red);
expect(result.backgroundColor, Colors.black);
expect(result.fontSize, 12);
expect(result.height, 10);
expect(result.wordSpacing, 1);
});
test('combine - return this', () {
final result = style.combine(null);
expect(result, style);
});
test('combine - return null with inherit', () {
final styleCopy = otherStyle.copyWith(inherit: false);
final result = style.combine(styleCopy);
expect(result, styleCopy);
});
});
}

View File

@ -0,0 +1,10 @@
import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('safeLaunchUrl without scheme', () async {
const href = null;
final result = await safeLaunchUrl(href);
expect(result, false);
});
}

View File

@ -145,6 +145,9 @@ extension on LogicalKeyboardKey {
if (this == LogicalKeyboardKey.underscore) {
return PhysicalKeyboardKey.minus;
}
if (this == LogicalKeyboardKey.tilde) {
return PhysicalKeyboardKey.backquote;
}
throw UnimplementedError();
}
}

View File

@ -1,6 +1,4 @@
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/extensions/text_node_extensions.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';

View File

@ -1,5 +1,4 @@
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/toolbar/toolbar_item_widget.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';

View File

@ -1,7 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.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';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
@ -13,8 +12,8 @@ void main() async {
group('selection_menu_widget.dart', () {
for (var i = 0; i < defaultSelectionMenuItems.length; i += 1) {
testWidgets('Selects number.$i item in selection menu with enter', (
tester) async {
testWidgets('Selects number.$i item in selection menu with enter',
(tester) async {
final editor = await _prepare(tester);
for (var j = 0; j < i; j++) {
await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
@ -30,8 +29,8 @@ void main() async {
}
});
testWidgets('Selects number.$i item in selection menu with click', (
tester) async {
testWidgets('Selects number.$i item in selection menu with click',
(tester) async {
final editor = await _prepare(tester);
await tester.tap(find.byType(SelectionMenuItemWidget).at(i));
@ -59,7 +58,7 @@ void main() async {
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(4),
findsNWidgets(5),
);
await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect(
@ -148,7 +147,8 @@ Future<void> _testDefaultSelectionMenuItems(
int index, EditorWidgetTester editor) async {
expect(editor.documentLength, 4);
expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0));
expect((editor.nodeAtPath([1]) as TextNode).toRawString(), 'Welcome to Appflowy 😁');
expect((editor.nodeAtPath([1]) as TextNode).toRawString(),
'Welcome to Appflowy 😁');
final node = editor.nodeAtPath([2]);
final item = defaultSelectionMenuItems[index];
final itemName = item.name();

View File

@ -2,7 +2,6 @@ import 'dart:collection';
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/flowy_rich_text.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:network_image_mock/network_image_mock.dart';

View File

@ -8,7 +8,6 @@ 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(() {

View File

@ -150,5 +150,111 @@ void main() async {
expect(textNode.toRawString(), text);
});
});
group('convert double tilde to strikethrough', () {
Future<void> insertTilde(
EditorWidgetTester editor, {
int repeat = 1,
}) async {
for (var i = 0; i < repeat; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.tilde,
isShiftPressed: true,
);
}
}
testWidgets('~~AppFlowy~~ to strikethrough AppFlowy', (tester) async {
const text = '~~AppFlowy~';
final editor = tester.editor..insertTextNode('');
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
final textNode = editor.nodeAtPath([0]) as TextNode;
for (var i = 0; i < text.length; i++) {
await editor.insertText(textNode, text[i], i);
}
await insertTilde(editor);
final allStrikethrough = textNode.allSatisfyStrikethroughInSelection(
Selection.single(
path: [0],
startOffset: 0,
endOffset: textNode.toRawString().length,
),
);
expect(allStrikethrough, true);
expect(textNode.toRawString(), 'AppFlowy');
});
testWidgets('App~~Flowy~~ to strikethrough AppFlowy', (tester) async {
const text = 'App~~Flowy~';
final editor = tester.editor..insertTextNode('');
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
final textNode = editor.nodeAtPath([0]) as TextNode;
for (var i = 0; i < text.length; i++) {
await editor.insertText(textNode, text[i], i);
}
await insertTilde(editor);
final allStrikethrough = textNode.allSatisfyStrikethroughInSelection(
Selection.single(
path: [0],
startOffset: 3,
endOffset: textNode.toRawString().length,
),
);
expect(allStrikethrough, true);
expect(textNode.toRawString(), 'AppFlowy');
});
testWidgets('~~~AppFlowy~~ to bold ~AppFlowy', (tester) async {
const text = '~~~AppFlowy~';
final editor = tester.editor..insertTextNode('');
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
final textNode = editor.nodeAtPath([0]) as TextNode;
for (var i = 0; i < text.length; i++) {
await editor.insertText(textNode, text[i], i);
}
await insertTilde(editor);
final allStrikethrough = textNode.allSatisfyStrikethroughInSelection(
Selection.single(
path: [0],
startOffset: 1,
endOffset: textNode.toRawString().length,
),
);
expect(allStrikethrough, true);
expect(textNode.toRawString(), '~AppFlowy');
});
testWidgets('~~~~ nothing changes', (tester) async {
const text = '~~~';
final editor = tester.editor..insertTextNode('');
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
final textNode = editor.nodeAtPath([0]) as TextNode;
for (var i = 0; i < text.length; i++) {
await editor.insertText(textNode, text[i], i);
}
await insertTilde(editor);
final allStrikethrough = textNode.allSatisfyStrikethroughInSelection(
Selection.single(
path: [0],
startOffset: 0,
endOffset: textNode.toRawString().length,
),
);
expect(allStrikethrough, false);
expect(textNode.toRawString(), text);
});
});
});
}

View File

@ -1,7 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.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';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';

View File

@ -3,7 +3,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespa
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(() {

View File

@ -4,7 +4,6 @@ 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(() {

View File

@ -2,6 +2,7 @@ import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flutter/services.dart';
class RoundedInputField extends StatefulWidget {
final String? hintText;
@ -24,6 +25,7 @@ class RoundedInputField extends StatefulWidget {
final FocusNode? focusNode;
final TextEditingController? controller;
final bool autoFocus;
final int? maxLength;
const RoundedInputField({
Key? key,
@ -47,6 +49,7 @@ class RoundedInputField extends StatefulWidget {
this.focusNode,
this.controller,
this.autoFocus = false,
this.maxLength,
}) : super(key: key);
@override
@ -89,6 +92,9 @@ class _RoundedInputFieldState extends State<RoundedInputField> {
initialValue: widget.initialValue,
focusNode: widget.focusNode,
autofocus: widget.autoFocus,
maxLength: widget.maxLength,
maxLengthEnforcement:
MaxLengthEnforcement.truncateAfterCompositionEnds,
onChanged: (value) {
inputText = value;
if (widget.onChanged != null) {

Some files were not shown because too many files have changed in this diff Show More