diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart index 4451a52b6f..3cc9bd1c72 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart @@ -20,7 +20,6 @@ const List _supportedFieldTypes = [ FieldType.URL, FieldType.LastEditedTime, FieldType.CreatedTime, - FieldType.Relation, ]; class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart index 69d7c07c09..4a1f1f05bc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart @@ -67,13 +67,13 @@ class _ChecklistItemsState extends State { (index, task) => Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), child: ChecklistItem( + key: ValueKey(task.data.id), task: task, autofocus: widget.state.newTask && index == tasks.length - 1, - onSubmitted: () { - if (index == tasks.length - 1) { - widget.bloc.add(const ChecklistCellEvent.createNewTask("")); - } - }, + onSubmitted: index == tasks.length - 1 + ? () => widget.bloc + .add(const ChecklistCellEvent.createNewTask("")) + : null, ), ), ) diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart index d32725c166..3ab883329a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart @@ -136,14 +136,6 @@ class _SelectTaskIntent extends Intent { const _SelectTaskIntent(); } -class _DeleteTaskIntent extends Intent { - const _DeleteTaskIntent(); -} - -class _StartEditingTaskIntent extends Intent { - const _StartEditingTaskIntent(); -} - class _EndEditingTaskIntent extends Intent { const _EndEditingTaskIntent(); } @@ -168,7 +160,9 @@ class ChecklistItem extends StatefulWidget { class _ChecklistItemState extends State { late final TextEditingController _textController; - final FocusNode _focusNode = FocusNode(); + final FocusNode _focusNode = FocusNode(skipTraversal: true); + final FocusNode _textFieldFocusNode = FocusNode(); + bool _isHovered = false; bool _isFocused = false; Timer? _debounceOnChanged; @@ -184,6 +178,7 @@ class _ChecklistItemState extends State { _debounceOnChanged?.cancel(); _textController.dispose(); _focusNode.dispose(); + _textFieldFocusNode.dispose(); super.dispose(); } @@ -200,6 +195,7 @@ class _ChecklistItemState extends State { @override Widget build(BuildContext context) { return FocusableActionDetector( + focusNode: _focusNode, onShowHoverHighlight: (isHovered) { setState(() => _isHovered = isHovered); }, @@ -212,116 +208,90 @@ class _ChecklistItemState extends State { .read() .add(ChecklistCellEvent.selectTask(widget.task.data.id)), ), - _DeleteTaskIntent: CallbackAction<_DeleteTaskIntent>( - onInvoke: (_DeleteTaskIntent intent) => context - .read() - .add(ChecklistCellEvent.deleteTask(widget.task.data.id)), - ), - _StartEditingTaskIntent: CallbackAction<_StartEditingTaskIntent>( - onInvoke: (_StartEditingTaskIntent intent) => - _focusNode.requestFocus(), - ), _EndEditingTaskIntent: CallbackAction<_EndEditingTaskIntent>( - onInvoke: (_EndEditingTaskIntent intent) => _focusNode.unfocus(), + onInvoke: (_EndEditingTaskIntent intent) => + _textFieldFocusNode.unfocus(), ), }, shortcuts: { - const SingleActivator(LogicalKeyboardKey.space): - const _SelectTaskIntent(), - const SingleActivator(LogicalKeyboardKey.delete): - const _DeleteTaskIntent(), - const SingleActivator(LogicalKeyboardKey.enter): - const _StartEditingTaskIntent(), - if (Platform.isMacOS) - const SingleActivator(LogicalKeyboardKey.enter, meta: true): - const _SelectTaskIntent() - else - const SingleActivator(LogicalKeyboardKey.enter, control: true): - const _SelectTaskIntent(), - const SingleActivator(LogicalKeyboardKey.arrowUp): - const PreviousFocusIntent(), - const SingleActivator(LogicalKeyboardKey.arrowDown): - const NextFocusIntent(), + SingleActivator( + LogicalKeyboardKey.enter, + meta: Platform.isMacOS, + control: !Platform.isMacOS, + ): const _SelectTaskIntent(), }, - descendantsAreTraversable: false, child: Container( constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight), decoration: BoxDecoration( - color: _isHovered || _isFocused || _focusNode.hasFocus + color: _isHovered || _isFocused || _textFieldFocusNode.hasFocus ? AFThemeExtension.of(context).lightGreyHover : Colors.transparent, borderRadius: Corners.s6Border, ), child: Row( children: [ - FlowyIconButton( - width: 32, - icon: FlowySvg( - widget.task.isSelected - ? FlowySvgs.check_filled_s - : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, + ExcludeFocus( + child: FlowyIconButton( + width: 32, + icon: FlowySvg( + widget.task.isSelected + ? FlowySvgs.check_filled_s + : FlowySvgs.uncheck_s, + blendMode: BlendMode.dst, + ), + hoverColor: Colors.transparent, + onPressed: () => context.read().add( + ChecklistCellEvent.selectTask(widget.task.data.id), + ), ), - hoverColor: Colors.transparent, - onPressed: () => context.read().add( - ChecklistCellEvent.selectTask(widget.task.data.id), - ), ), Expanded( child: Shortcuts( - shortcuts: { - const SingleActivator(LogicalKeyboardKey.space): - const DoNothingAndStopPropagationIntent(), - const SingleActivator(LogicalKeyboardKey.delete): - const DoNothingAndStopPropagationIntent(), - if (Platform.isMacOS) - LogicalKeySet( - LogicalKeyboardKey.fn, - LogicalKeyboardKey.backspace, - ): const DoNothingAndStopPropagationIntent(), - const SingleActivator(LogicalKeyboardKey.enter): - const DoNothingAndStopPropagationIntent(), - const SingleActivator(LogicalKeyboardKey.escape): - const _EndEditingTaskIntent(), - const SingleActivator(LogicalKeyboardKey.arrowUp): - const DoNothingAndStopPropagationIntent(), - const SingleActivator(LogicalKeyboardKey.arrowDown): - const DoNothingAndStopPropagationIntent(), + shortcuts: const { + SingleActivator(LogicalKeyboardKey.escape): + _EndEditingTaskIntent(), }, - child: TextField( - controller: _textController, - focusNode: _focusNode, - autofocus: widget.autofocus, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - border: InputBorder.none, - isCollapsed: true, - contentPadding: EdgeInsets.only( - top: 8.0, - bottom: 8.0, - left: 2.0, - right: _isHovered ? 2.0 : 8.0, - ), - hintText: LocaleKeys.grid_checklist_taskHint.tr(), - ), - onChanged: (text) { - if (_textController.value.composing.isCollapsed) { - _debounceOnChangedText(text); - } - }, - onSubmitted: (description) { - _submitUpdateTaskDescription(description); - widget.onSubmitted?.call(); + child: Builder( + builder: (context) { + return TextField( + controller: _textController, + focusNode: _textFieldFocusNode, + autofocus: widget.autofocus, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + border: InputBorder.none, + isCollapsed: true, + contentPadding: EdgeInsets.only( + top: 8.0, + bottom: 8.0, + left: 2.0, + right: _isHovered ? 2.0 : 8.0, + ), + hintText: LocaleKeys.grid_checklist_taskHint.tr(), + ), + textInputAction: widget.onSubmitted == null + ? TextInputAction.next + : null, + onChanged: (text) { + if (_textController.value.composing.isCollapsed) { + _debounceOnChangedText(text); + } + }, + onSubmitted: (description) { + _submitUpdateTaskDescription(description); + if (widget.onSubmitted != null) { + widget.onSubmitted?.call(); + } else { + Actions.invoke(context, const NextFocusIntent()); + } + }, + ); }, ), ), ), - if (_isHovered || _isFocused || _focusNode.hasFocus) - FlowyIconButton( - width: 32, - icon: const FlowySvg(FlowySvgs.delete_s), - hoverColor: Colors.transparent, - iconColorOnHover: Theme.of(context).colorScheme.error, + if (_isHovered || _isFocused || _textFieldFocusNode.hasFocus) + _DeleteTaskButton( onPressed: () => context.read().add( ChecklistCellEvent.deleteTask(widget.task.data.id), ), @@ -440,3 +410,56 @@ class _NewTaskItemState extends State { ); } } + +class _DeleteTaskButton extends StatefulWidget { + const _DeleteTaskButton({ + required this.onPressed, + }); + + final VoidCallback onPressed; + + @override + State<_DeleteTaskButton> createState() => _DeleteTaskButtonState(); +} + +class _DeleteTaskButtonState extends State<_DeleteTaskButton> { + final _materialStatesController = MaterialStatesController(); + + @override + void dispose() { + _materialStatesController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: widget.onPressed, + onHover: (_) => setState(() {}), + onFocusChange: (_) => setState(() {}), + style: ButtonStyle( + fixedSize: const MaterialStatePropertyAll(Size.square(32)), + minimumSize: const MaterialStatePropertyAll(Size.square(32)), + maximumSize: const MaterialStatePropertyAll(Size.square(32)), + overlayColor: MaterialStateProperty.resolveWith((state) { + if (state.contains(MaterialState.focused)) { + return AFThemeExtension.of(context).greyHover; + } + return Colors.transparent; + }), + shape: const MaterialStatePropertyAll( + RoundedRectangleBorder(borderRadius: Corners.s6Border), + ), + ), + statesController: _materialStatesController, + child: FlowySvg( + FlowySvgs.delete_s, + color: _materialStatesController.value + .contains(MaterialState.hovered) || + _materialStatesController.value.contains(MaterialState.focused) + ? Theme.of(context).colorScheme.error + : null, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart index 010c7cd6f9..116158b10b 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart @@ -38,7 +38,7 @@ class FlowyIconButton extends StatelessWidget { this.preferBelow = true, this.isSelected, required this.icon, - }) : assert((richTooltipText != null && tooltipText == null) || + }) : assert((richTooltipText != null && tooltipText == null) || (richTooltipText == null && tooltipText != null) || (richTooltipText == null && tooltipText == null)); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx index 84f73c8ebe..34a99007ad 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx @@ -3,7 +3,7 @@ import TextField from '@mui/material/TextField'; import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; -const urlPattern = /^https?:\/\/.+/; +const urlPattern = /^(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.gif|.webm|.webp|.svg)(\?[^\s[",><]*)?$/; export function EmbedLink({ onDone, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx index bf29f68a2e..d94e5f2889 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx @@ -22,8 +22,10 @@ export const LocalImage = forwardRef< const { readBinaryFile, BaseDirectory } = await import('@tauri-apps/api/fs'); try { + const svg = src.endsWith('.svg'); + const buffer = await readBinaryFile(src, { dir: BaseDirectory.AppLocalData }); - const blob = new Blob([buffer]); + const blob = new Blob([buffer], { type: svg ? 'image/svg+xml' : 'image' }); setImageURL(URL.createObjectURL(blob)); } catch (e) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx index 60ea7d1c05..7db90c4e8f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx @@ -50,6 +50,7 @@ export interface KeyboardNavigationProps { onBlur?: () => void; itemClassName?: string; itemStyle?: React.CSSProperties; + renderNoResult?: () => React.ReactNode; } function KeyboardNavigation({ @@ -69,6 +70,7 @@ function KeyboardNavigation({ onFocus, itemClassName, itemStyle, + renderNoResult, }: KeyboardNavigationProps) { const { t } = useTranslation(); const ref = useRef(null); @@ -301,6 +303,8 @@ function KeyboardNavigation({ > {options.length > 0 ? ( options.map(renderOption) + ) : renderNoResult ? ( + renderNoResult() ) : ( {t('findAndReplace.noResult')} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx index 762a9dbf08..027936d280 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx @@ -4,7 +4,17 @@ import { useViewId } from '$app/hooks'; import { Button } from '@mui/material'; import { useTranslation } from 'react-i18next'; -function AddNewOption({ rowId, fieldId, onClose }: { rowId: string; fieldId: string; onClose: () => void }) { +function AddNewOption({ + rowId, + fieldId, + onClose, + onFocus, +}: { + rowId: string; + fieldId: string; + onClose: () => void; + onFocus: () => void; +}) { const { t } = useTranslation(); const [value, setValue] = useState(''); const viewId = useViewId(); @@ -18,6 +28,7 @@ function AddNewOption({ rowId, fieldId, onClose }: { rowId: string; fieldId: str return (
(null); + const [focusedId, setFocusedId] = useState(null); return ( - { - if (e.key === 'Escape') { - e.stopPropagation(); - e.preventDefault(); - props.onClose?.({}, 'escapeKeyDown'); - } - }} - > +
setHoverId(null)} className={'flex h-full w-full flex-col overflow-hidden'} > {options.length > 0 && ( @@ -56,10 +45,10 @@ function ChecklistCellActions({ setHoverId(option.id)} + isSelected={focusedId === option.id} key={option.id} option={option} + onFocus={() => setFocusedId(option.id)} onClose={() => props.onClose?.({}, 'escapeKeyDown')} checked={selectedOptions?.includes(option.id) || false} /> @@ -71,7 +60,14 @@ function ChecklistCellActions({ )} - props.onClose?.({}, 'escapeKeyDown')} fieldId={fieldId} rowId={rowId} /> + { + setFocusedId(null); + }} + onClose={() => props.onClose?.({}, 'escapeKeyDown')} + fieldId={fieldId} + rowId={rowId} + />
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx index b8bfa13f4a..5c6a55fa60 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx @@ -18,17 +18,18 @@ function ChecklistItem({ rowId, fieldId, onClose, - isHovered, - onMouseEnter, + isSelected, + onFocus, }: { checked: boolean; option: SelectOption; rowId: string; fieldId: string; onClose: () => void; - isHovered: boolean; - onMouseEnter: () => void; + isSelected: boolean; + onFocus: () => void; }) { + const inputRef = React.useRef(null); const { t } = useTranslation(); const [value, setValue] = useState(option.name); const viewId = useViewId(); @@ -61,17 +62,23 @@ function ChecklistItem({ return (
-
+
{checked ? : }
{ if (e.key === 'Escape') { @@ -99,14 +106,7 @@ function ChecklistItem({ }} />
- +
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx index 10147630eb..63b5872c2e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx @@ -118,6 +118,9 @@ export const SelectOptionModifyMenu: FC = ({ fieldId, opt onClick={(e) => { e.stopPropagation(); }} + onMouseDown={(e) => { + e.stopPropagation(); + }} autoFocus={true} placeholder={t('grid.selectOption.tagName')} size='small' diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx index f66f2a5545..e2cd27019f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx @@ -54,7 +54,7 @@ function SelectCellActions({ ), })); - if (result.length === 0) { + if (result.length === 0 && newOptionName) { result.push({ key: CREATE_OPTION_KEY, content: , @@ -69,8 +69,7 @@ function SelectCellActions({ const updateCell = useCallback( async (optionIds: string[]) => { if (!cell || !rowId) return; - const prev = selectedOptionIds; - const deleteOptionIds = prev?.filter((id) => optionIds.find((cur) => cur === id) === undefined); + const deleteOptionIds = selectedOptionIds?.filter((id) => optionIds.find((cur) => cur === id) === undefined); await cellService.updateSelectCell(viewId, rowId, field.id, { insertOptionIds: optionIds, @@ -136,9 +135,12 @@ function SelectCellActions({
-
- {shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')} -
+ {filteredOptions.length > 0 && ( +
+ {shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')} +
+ )} +
null} />
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx index 4aafe3e308..005d185c8f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { Popover, TextareaAutosize } from '@mui/material'; interface Props { @@ -8,6 +8,7 @@ interface Props { text: string; onInput: (event: React.FormEvent) => void; } + function EditTextCellInput({ editing, anchorEl, onClose, text, onInput }: Props) { const handleEnter = (e: React.KeyboardEvent) => { const shift = e.shiftKey; @@ -20,6 +21,13 @@ function EditTextCellInput({ editing, anchorEl, onClose, text, onInput }: Props) } }; + const setRef = useCallback((e: HTMLTextAreaElement | null) => { + if (!e) return; + const selectionStart = e.value.length; + + e.setSelectionRange(selectionStart, selectionStart); + }, []); + return ( (null); + const [newWidth, setNewWidth] = useState(imageWidth ?? null); - const handleWidthChange = useCallback( - (newWidth: number) => { + const debounceSubmitWidth = useMemo(() => { + return debounce((newWidth: number) => { CustomEditor.setImageBlockData(editor, node, { width: newWidth, }); + }, DELAY); + }, [editor, node]); + + const handleWidthChange = useCallback( + (newWidth: number) => { + setNewWidth(newWidth); + debounceSubmitWidth(newWidth); }, - [editor, node] + [debounceSubmitWidth] ); useEffect(() => { @@ -38,7 +51,7 @@ function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) }, [hasError, initialWidth, loading]); const imageProps: React.ImgHTMLAttributes = useMemo(() => { return { - style: { width: loading || hasError ? '0' : imageWidth ?? '100%', opacity: selected ? 0.8 : 1 }, + style: { width: loading || hasError ? '0' : newWidth ?? '100%', opacity: selected ? 0.8 : 1 }, className: 'object-cover', ref: imgRef, src: url, @@ -52,7 +65,7 @@ function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) setLoading(false); }, }; - }, [url, imageWidth, loading, hasError, selected]); + }, [url, newWidth, loading, hasError, selected]); const renderErrorNode = useCallback(() => { return ( @@ -75,7 +88,13 @@ function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) onMouseLeave={() => { setShowActions(false); }} - className={`relative min-h-[48px] ${hasError || (loading && source !== ImageType.Local) ? 'w-full' : ''}`} + style={{ + minWidth: MIN_WIDTH, + width: 'fit-content', + }} + className={`image-render relative min-h-[48px] ${ + hasError || (loading && source !== ImageType.Local) ? 'w-full' : '' + }`} > {source === ImageType.Local ? ( )} - {initialWidth && } + {initialWidth && ( + <> + + + + )} {showActions && } {hasError ? ( renderErrorNode() diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx index ff241937de..e0d272acf3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx @@ -1,24 +1,32 @@ import React, { useCallback, useRef } from 'react'; -const MIN_WIDTH = 80; - -function ImageResizer({ width, onWidthChange }: { width: number; onWidthChange: (newWidth: number) => void }) { +function ImageResizer({ + minWidth, + width, + onWidthChange, + isLeft, +}: { + isLeft?: boolean; + minWidth: number; + width: number; + onWidthChange: (newWidth: number) => void; +}) { const originalWidth = useRef(width); const startX = useRef(0); const onResize = useCallback( (e: MouseEvent) => { e.preventDefault(); - const diff = e.clientX - startX.current; + const diff = isLeft ? startX.current - e.clientX : e.clientX - startX.current; const newWidth = originalWidth.current + diff; - if (newWidth < MIN_WIDTH) { + if (newWidth < minWidth) { return; } onWidthChange(newWidth); }, - [onWidthChange] + [isLeft, minWidth, onWidthChange] ); const onResizeEnd = useCallback(() => { @@ -40,7 +48,8 @@ function ImageResizer({ width, onWidthChange }: { width: number; onWidthChange:
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx index 20602b2637..3d7dff3cb4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx @@ -44,6 +44,16 @@ function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaul if (!input) return; + let isComposing = false; + + const handleCompositionUpdate = () => { + isComposing = true; + }; + + const handleCompositionEnd = () => { + isComposing = false; + }; + const handleKeyDown = (e: KeyboardEvent) => { e.stopPropagation(); @@ -69,16 +79,22 @@ function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaul return; } - if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + if (!isComposing && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { notify.clear(); notify.info(`Press Tab to focus on the menu`); return; } }; + input.addEventListener('compositionstart', handleCompositionUpdate); + input.addEventListener('compositionend', handleCompositionEnd); + input.addEventListener('compositionupdate', handleCompositionUpdate); input.addEventListener('keydown', handleKeyDown); return () => { input.removeEventListener('keydown', handleKeyDown); + input.removeEventListener('compositionstart', handleCompositionUpdate); + input.removeEventListener('compositionend', handleCompositionEnd); + input.removeEventListener('compositionupdate', handleCompositionUpdate); }; }, [link, onClose, setNodeMark]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss index 36adff3880..e26d842317 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss @@ -138,19 +138,24 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { height: 0; } -.image-resizer { - @apply absolute w-[10px] top-0 z-10 flex h-full cursor-col-resize items-center justify-end; - .resize-handle { - @apply h-1/4 w-1/2 transform transition-all duration-500 select-none rounded-full border border-white opacity-0; - background: var(--fill-toolbar); +.image-render { + .image-resizer { + @apply absolute w-[10px] top-0 z-10 flex h-full cursor-col-resize items-center justify-end; + .resize-handle { + @apply h-1/4 w-1/2 transform transition-all duration-500 select-none rounded-full border border-white opacity-0; + background: var(--fill-toolbar); + } } &:hover { - .resize-handle { - @apply opacity-90; + .image-resizer{ + .resize-handle { + @apply opacity-90; + } } } } + .image-block, .math-equation-block, [data-dark-mode="true"] .image-block, [data-dark-mode="true"] .math-equation-block { ::selection { @apply bg-transparent; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts index 7d92eddd91..22213ac8b3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts @@ -1,5 +1,5 @@ export const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB -export const ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png']; +export const ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp']; export const IMAGE_DIR = 'images'; export function getFileName(url: string) { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index a59768c013..2e60ac8be7 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -921,7 +921,7 @@ "error": { "invalidImage": "Invalid image", "invalidImageSize": "Image size must be less than 5MB", - "invalidImageFormat": "Image format is not supported. Supported formats: JPEG, PNG, JPG", + "invalidImageFormat": "Image format is not supported. Supported formats: JPEG, PNG, JPG, GIF, SVG, WEBP", "invalidImageUrl": "Invalid image URL", "noImage": "No such file or directory" },