fix: bookmark popper menu only display after pasted (#2578)

This commit is contained in:
Qi
2023-05-29 22:25:30 +08:00
committed by GitHub
parent 58ba11e13c
commit 402d12a0e1
9 changed files with 197 additions and 173 deletions

View File

@@ -1,33 +1,14 @@
import { MenuItem, MuiClickAwayListener, PureMenu } from '@affine/component';
import type { EditorPlugin } from '@affine/component/block-suite-editor';
import type { SerializedBlock } from '@blocksuite/blocks';
import {
getCurrentBlockRange,
getCurrentNativeRange,
getVirgoByModel,
hasNativeSelection,
} from '@blocksuite/blocks/std';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { useEffect, useMemo, useRef, useState } from 'react';
const isCursorInLink = (page: Page) => {
if (!hasNativeSelection()) return false;
const blockRange = getCurrentBlockRange(page);
if (
!blockRange ||
blockRange.type !== 'Native' ||
blockRange.startOffset !== blockRange.endOffset
) {
return false;
}
const {
models: [model],
} = blockRange;
const vEditor = getVirgoByModel(model);
const delta = vEditor?.getDeltaByRangeIndex(blockRange.startOffset);
return delta?.attributes?.link;
};
import { useCallback, useEffect, useMemo, useState } from 'react';
type ShortcutMap = {
[key: string]: (e: KeyboardEvent, page: Page) => void;
@@ -94,10 +75,25 @@ const handleEnter = ({
return callback();
};
const shouldShowBookmarkMenu = (pastedBlocks: SerializedBlock[]) => {
if (!pastedBlocks.length || pastedBlocks.length > 1) {
return;
}
const [firstBlock] = pastedBlocks;
if (
!firstBlock.text ||
!firstBlock.text.length ||
firstBlock.text.length > 1
) {
return;
}
return !!firstBlock.text[0].attributes?.link;
};
const BookMarkMenu: EditorPlugin['render'] = ({ page }) => {
const [anchor, setAnchor] = useState<Range | null>(null);
const [selectedOption, setSelectedOption] = useState<string>('');
const shouldHijack = useRef(false);
const [selectedOption, setSelectedOption] = useState<string>(
menuOptions[0].id
);
const shortcutMap = useMemo<ShortcutMap>(
() => ({
ArrowUp: () => {
@@ -127,51 +123,57 @@ const BookMarkMenu: EditorPlugin['render'] = ({ page }) => {
page,
selectedOption,
callback: () => {
shouldHijack.current = false;
setAnchor(null);
},
}),
Escape: () => {
setAnchor(null);
},
}),
[page, selectedOption]
);
useEffect(() => {
// TODO: textUpdated slot is not working
// const disposer = page.slots.textUpdated.on(() => {
// console.log('text Updated', page);
// });
const disposer = page.slots.historyUpdated.on(() => {
if (!isCursorInLink(page)) {
return;
}
setAnchor(getCurrentNativeRange());
shouldHijack.current = true;
});
return () => {
// disposer.dispose();
disposer.dispose();
};
}, [page, shortcutMap]);
useEffect(() => {
const keydown = (e: KeyboardEvent) => {
if (!shouldHijack.current) {
return;
}
const onKeydown = useCallback(
(e: KeyboardEvent) => {
const shortcut = shortcutMap[e.key];
if (shortcut) {
e.stopPropagation();
e.preventDefault();
shortcut(e, page);
} else {
setAnchor(null);
}
};
document.addEventListener('keydown', keydown, { capture: true });
},
[page, shortcutMap]
);
useEffect(() => {
const disposer = page.slots.pasted.on(pastedBlocks => {
if (!shouldShowBookmarkMenu(pastedBlocks)) {
return;
}
setTimeout(() => {
setAnchor(getCurrentNativeRange());
}, 100);
});
return () => {
document.removeEventListener('keydown', keydown, { capture: true });
disposer.dispose();
};
}, [page, shortcutMap]);
}, [onKeydown, page, shortcutMap]);
useEffect(() => {
if (anchor) {
document.addEventListener('keydown', onKeydown, { capture: true });
} else {
// reset status and remove event
setSelectedOption(menuOptions[0].id);
document.removeEventListener('keydown', onKeydown, { capture: true });
}
return () => {
document.removeEventListener('keydown', onKeydown, { capture: true });
};
}, [anchor, onKeydown]);
return anchor ? (
<MuiClickAwayListener
@@ -187,7 +189,19 @@ const BookMarkMenu: EditorPlugin['render'] = ({ page }) => {
<MenuItem
key={id}
active={selectedOption === id}
onClick={() => {}}
onClick={() => {
handleEnter({
page,
selectedOption: id,
callback: () => {
setAnchor(null);
},
});
}}
disableHover={true}
onMouseEnter={() => {
setSelectedOption(id);
}}
>
{label}
</MenuItem>

View File

@@ -14,6 +14,7 @@ export type IconMenuProps = PropsWithChildren<{
iconSize?: [number, number];
disabled?: boolean;
active?: boolean;
disableHover?: boolean;
}> &
HTMLAttributes<HTMLButtonElement>;

View File

@@ -52,42 +52,51 @@ export const StyledMenuItem = styled('button')<{
isDir?: boolean;
disabled?: boolean;
active?: boolean;
}>(({ isDir = false, disabled = false, active = false }) => {
return {
width: '100%',
borderRadius: '5px',
padding: '0 14px',
fontSize: 'var(--affine-font-base)',
height: '32px',
...displayFlex('flex-start', 'center'),
cursor: isDir ? 'pointer' : '',
position: 'relative',
backgroundColor: 'transparent',
color: disabled
? 'var(--affine-text-disable-color)'
: 'var(--affine-text-primary-color)',
svg: {
disableHover?: boolean;
}>(
({
isDir = false,
disabled = false,
active = false,
disableHover = false,
}) => {
return {
width: '100%',
borderRadius: '5px',
padding: '0 14px',
fontSize: 'var(--affine-font-base)',
height: '32px',
...displayFlex('flex-start', 'center'),
cursor: isDir ? 'pointer' : '',
position: 'relative',
backgroundColor: 'transparent',
color: disabled
? 'var(--affine-text-disable-color)'
: 'var(--affine-icon-color)',
},
...(disabled
? {
cursor: 'not-allowed',
pointerEvents: 'none',
}
: {}),
: 'var(--affine-text-primary-color)',
svg: {
color: disabled
? 'var(--affine-text-disable-color)'
: 'var(--affine-icon-color)',
},
...(disabled
? {
cursor: 'not-allowed',
pointerEvents: 'none',
}
: {}),
':hover': disabled
? {}
: {
backgroundColor: 'var(--affine-hover-color)',
},
':hover':
disabled || disableHover
? {}
: {
backgroundColor: 'var(--affine-hover-color)',
},
...(active && !disabled
? {
backgroundColor: 'var(--affine-hover-color)',
}
: {}),
};
});
...(active && !disabled
? {
backgroundColor: 'var(--affine-hover-color)',
}
: {}),
};
}
);