Skip to content

Commit

Permalink
Merge branch 'main' into feat-zhiya
Browse files Browse the repository at this point in the history
* main:
  feat: table navigation using TAB key (AppFlowy-IO#627)
  feat: add markdown link syntax formatting (AppFlowy-IO#618)
  fix:text_decoration_mobile_toolbar_padding (AppFlowy-IO#621)
  fix: active hover on upload image (AppFlowy-IO#597)
  feat: adding an ability to have a link check before embedding (AppFlowy-IO#603)
  fix: node_iterator toList encounter Dangling Node trigger dead loop. (AppFlowy-IO#623)
  • Loading branch information
q200892907 committed Jan 4, 2024
2 parents df11795 + 7ec43c9 commit bd0961d
Show file tree
Hide file tree
Showing 11 changed files with 639 additions and 68 deletions.
5 changes: 4 additions & 1 deletion lib/src/core/document/node_iterator.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/document/document.dart';
import 'package:appflowy_editor/src/core/document/node.dart';

/// [NodeIterator] is used to traverse the nodes in visual order.
class NodeIterator implements Iterator<Node> {
Expand Down Expand Up @@ -47,6 +47,9 @@ class NodeIterator implements Iterator<Node> {
_currentNode = node.children.first;
} else if (node.next != null) {
_currentNode = node.next!;
} else if (node.parent == null) {
_currentNode = null;
return false;
} else {
while (node.parent != null) {
node = node.parent!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,66 +288,69 @@ class _UploadImageMenuState extends State<UploadImageMenu> {

Widget _buildFileUploadContainer(BuildContext context) {
return Expanded(
child: GestureDetector(
onTap: () async {
final result = await _filePicker.pickFiles(
dialogTitle: '',
allowMultiple: false,
type: kIsWeb ? fp.FileType.custom : fp.FileType.image,
allowedExtensions: allowedExtensions,
withData: kIsWeb,
);
if (result != null && result.files.isNotEmpty) {
setState(() {
final bytes = result.files.first.bytes;
if (kIsWeb && bytes != null) {
_imagePathOrContent = base64String(bytes);
} else {
_imagePathOrContent = result.files.first.path;
}
});
}
},
child: Container(
height: 60,
margin: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xff00BCF0)),
borderRadius: BorderRadius.circular(12.0),
),
child: _imagePathOrContent != null
? Align(
alignment: Alignment.center,
child: kIsWeb
? Image.memory(
dataFromBase64String(_imagePathOrContent!),
fit: BoxFit.cover,
)
: Image.file(
File(_imagePathOrContent!),
fit: BoxFit.cover,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () async {
final result = await _filePicker.pickFiles(
dialogTitle: '',
allowMultiple: false,
type: kIsWeb ? fp.FileType.custom : fp.FileType.image,
allowedExtensions: allowedExtensions,
withData: kIsWeb,
);
if (result != null && result.files.isNotEmpty) {
setState(() {
final bytes = result.files.first.bytes;
if (kIsWeb && bytes != null) {
_imagePathOrContent = base64String(bytes);
} else {
_imagePathOrContent = result.files.first.path;
}
});
}
},
child: Container(
height: 60,
margin: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xff00BCF0)),
borderRadius: BorderRadius.circular(12.0),
),
child: _imagePathOrContent != null
? Align(
alignment: Alignment.center,
child: kIsWeb
? Image.memory(
dataFromBase64String(_imagePathOrContent!),
fit: BoxFit.cover,
)
: Image.file(
File(_imagePathOrContent!),
fit: BoxFit.cover,
),
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const EditorSvg(
name: 'upload_image',
width: 32,
height: 32,
),
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const EditorSvg(
name: 'upload_image',
width: 32,
height: 32,
),
const SizedBox(height: 8.0),
Text(
AppFlowyEditorL10n.current.chooseImage,
style: const TextStyle(
fontSize: 14.0,
color: Color(0xff00BCF0),
const SizedBox(height: 8.0),
Text(
AppFlowyEditorL10n.current.chooseImage,
style: const TextStyle(
fontSize: 14.0,
color: Color(0xff00BCF0),
),
),
),
],
],
),
),
),
),
),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ final List<CommandShortcutEvent> tableCommands = [
_rightInTableCell,
_upInTableCell,
_downInTableCell,
_tabInTableCell,
_shiftTabInTableCell,
_backSpaceInTableCell,
];

Expand Down Expand Up @@ -41,6 +43,18 @@ final CommandShortcutEvent _downInTableCell = CommandShortcutEvent(
handler: _downInTableCellHandler,
);

final CommandShortcutEvent _tabInTableCell = CommandShortcutEvent(
key: 'Navigate around the cells at same offset',
command: 'tab',
handler: _tabInTableCellHandler,
);

final CommandShortcutEvent _shiftTabInTableCell = CommandShortcutEvent(
key: 'Navigate around the cells at same offset in reverse',
command: 'shift+tab',
handler: _shiftTabInTableCellHandler,
);

final CommandShortcutEvent _backSpaceInTableCell = CommandShortcutEvent(
key: 'Stop at the beginning of the cell',
command: 'backspace',
Expand Down Expand Up @@ -80,7 +94,7 @@ CommandShortcutEventHandler _leftInTableCellHandler = (editorState) {
final selection = editorState.selection;
if (_hasSelectionAndTableCell(inTableNodes, selection) &&
selection!.start.offset == 0) {
final nextNode = _getNextNode(inTableNodes, -1, 0);
final nextNode = _getPreviousNode(inTableNodes, 1, 0);
if (_nodeHasTextChild(nextNode)) {
final target = nextNode!.childAtIndexOrNull(0)!;
editorState.selectionService.updateSelection(
Expand Down Expand Up @@ -152,6 +166,44 @@ CommandShortcutEventHandler _downInTableCellHandler = (editorState) {
return KeyEventResult.ignored;
};

CommandShortcutEventHandler _tabInTableCellHandler = (editorState) {
final inTableNodes = _inTableNodes(editorState);
final selection = editorState.selection;
if (_hasSelectionAndTableCell(inTableNodes, selection)) {
final nextNode = _getNextNode(inTableNodes, 1, 0);
if (nextNode != null && _nodeHasTextChild(nextNode)) {
final firstChild = nextNode.childAtIndexOrNull(0);
if (firstChild != null) {
editorState.selection = Selection.single(
path: firstChild.path,
startOffset: 0,
);
}
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};

CommandShortcutEventHandler _shiftTabInTableCellHandler = (editorState) {
final inTableNodes = _inTableNodes(editorState);
final selection = editorState.selection;
if (_hasSelectionAndTableCell(inTableNodes, selection)) {
final previousNode = _getPreviousNode(inTableNodes, 1, 0);
if (previousNode != null && _nodeHasTextChild(previousNode)) {
final firstChild = previousNode.childAtIndexOrNull(0);
if (firstChild != null) {
editorState.selection = Selection.single(
path: firstChild.path,
startOffset: 0,
);
}
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};

CommandShortcutEventHandler _backspaceInTableCellHandler = (editorState) {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
Expand Down Expand Up @@ -191,15 +243,59 @@ bool _hasSelectionAndTableCell(
selection.isCollapsed &&
nodes.first.parent?.type == TableCellBlockKeys.type;

Node? _getNextNode(Iterable<Node> nodes, int colDiff, rowDiff) {
Node? _getNextNode(Iterable<Node> nodes, int colDiff, int rowDiff) {
final cell = nodes.first.parent!;
final col = cell.attributes[TableCellBlockKeys.colPosition];
final row = cell.attributes[TableCellBlockKeys.rowPosition];
return cell.parent != null
? getCellNode(cell.parent!, col + colDiff, row + rowDiff)
final table = cell.parent;
if (table == null) {
return null;
}

final numCols =
table.children.last.attributes[TableCellBlockKeys.colPosition] + 1;
final numRows =
table.children.last.attributes[TableCellBlockKeys.rowPosition] + 1;

// Calculate the next column index, considering the column difference and wrapping around with modulo.
var nextCol = (col + colDiff) % numCols;

// Calculate the next row index, taking into account the row difference and adjusting for additional rows due to column change.
var nextRow = row + rowDiff + ((col + colDiff) ~/ numCols);

return isValidPosition(nextCol, nextRow, numCols, numRows)
? getCellNode(table, nextCol, nextRow)
: null;
}

Node? _getPreviousNode(Iterable<Node> nodes, int colDiff, int rowDiff) {
final cell = nodes.first.parent!;
final col = cell.attributes[TableCellBlockKeys.colPosition];
final row = cell.attributes[TableCellBlockKeys.rowPosition];
final table = cell.parent;
if (table == null) {
return null;
}

final numCols =
table.children.last.attributes[TableCellBlockKeys.colPosition] + 1;
final numRows =
table.children.last.attributes[TableCellBlockKeys.rowPosition] + 1;

// Calculate the previous column index, ensuring it wraps within the table boundaries using modulo.
var prevCol = (col - colDiff + numCols) % numCols;

// Calculate the previous row index, considering table boundaries and adjusting for potential column underflow.
var prevRow = row - rowDiff - ((col - colDiff) < 0 ? 1 : 0);

return isValidPosition(prevCol, prevRow, numCols, numRows)
? getCellNode(table, prevCol, prevRow)
: null;
}

bool isValidPosition(int col, int row, int numCols, int numRows) =>
col >= 0 && col < numCols && row >= 0 && row < numRows;

bool _nodeHasTextChild(Node? n) =>
n != null &&
n.children.isNotEmpty &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export 'format_single_character/format_italic.dart';
export 'format_single_character/format_single_character.dart';
export 'format_single_character/format_strikethrough.dart';
export 'insert_newline.dart';
export 'markdown_link_shortcut_event.dart';
export 'markdown_syntax_character_shortcut_events.dart';
export 'slash_command.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'package:appflowy_editor/appflowy_editor.dart';

/// format the markdown link syntax to hyperlink
final CharacterShortcutEvent formatMarkdownLinkToLink = CharacterShortcutEvent(
key: 'format the text surrounded by double asterisks to bold',
character: ')',
handler: (editorState) async => handleFormatMarkdownLinkToLink(
editorState: editorState,
),
);

final _linkRegex = RegExp(r'\[([^\]]*)\]\((.*?)\)');

bool handleFormatMarkdownLinkToLink({
required EditorState editorState,
}) {
final selection = editorState.selection;
// if the selection is not collapsed or the cursor is at the first 5 index range, we don't need to format it.
// we should return false to let the IME handle it.
if (selection == null || !selection.isCollapsed || selection.end.offset < 6) {
return false;
}

final path = selection.end.path;
final node = editorState.getNodeAtPath(path);
final delta = node?.delta;
// if the node doesn't contain the delta(which means it isn't a text)
// we don't need to format it.
if (node == null || delta == null) {
return false;
}

final plainText = '${delta.toPlainText()})';

// Determine if regex matches the plainText.
if (!_linkRegex.hasMatch(plainText)) {
return false;
}

final matches = _linkRegex.allMatches(plainText);
final lastMatch = matches.last;
final title = lastMatch.group(1);
final link = lastMatch.group(2);

// if all the conditions are met, we should format the text to a link.
final transaction = editorState.transaction
..deleteText(
node,
lastMatch.start,
lastMatch.end - lastMatch.start - 1,
)
..insertText(
node,
lastMatch.start,
title!,
attributes: {
AppFlowyRichTextKeys.href: link,
},
);
editorState.apply(transaction);

return true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ final List<CharacterShortcutEvent> markdownSyntaxShortcutEvents = [

// format -- into em dash
formatDoubleHyphenEmDash,

// format [*](*) to link
formatMarkdownLinkToLink,
];
Loading

0 comments on commit bd0961d

Please sign in to comment.