feat: 增加 QuickSwitcher

pull/13/head
moonrailgun 4 years ago
parent 3157a392aa
commit 3329382415

@ -16,6 +16,7 @@
"k249e23b9": "E-mail format is incorrect",
"k24ccd723": "Refresh now",
"k267cc491": "Me",
"k2a8031e": "Homepage",
"k2d6cfb27": "Chat Channel",
"k2ec4966c": "Selected {{num}} items",
"k3172297b": "This feature is not yet open",
@ -53,6 +54,7 @@
"k5bec387": "Unable to get group information",
"k5f91e72c": "Built Plugins",
"k61a1db2": "Already applied",
"k621f7e70": "Quick search, jump",
"k62f009e7": "Modify group name success",
"k630ccbcb": "Plugin install successed",
"k63857abf": "Confirm Create",

@ -16,6 +16,7 @@
"k249e23b9": "邮箱格式不正确",
"k24ccd723": "立即刷新",
"k267cc491": "我",
"k2a8031e": "个人主页",
"k2d6cfb27": "聊天频道",
"k2ec4966c": "已选择 {{num}} 项",
"k3172297b": "该功能暂未开放",
@ -53,6 +54,7 @@
"k5bec387": "无法获取到群组信息",
"k5f91e72c": "内置插件",
"k61a1db2": "已申请",
"k621f7e70": "快速搜索、跳转",
"k62f009e7": "修改群组名成功",
"k630ccbcb": "插件安装成功",
"k63857abf": "确认创建",

@ -48,7 +48,7 @@
"@testing-library/react-hooks": "^7.0.1",
"@types/copy-webpack-plugin": "^8.0.0",
"@types/dts-generator": "^2.1.6",
"@types/is-hotkey": "^0.1.3",
"@types/is-hotkey": "^0.1.5",
"@types/mini-css-extract-plugin": "^1.4.3",
"@types/node": "^15.12.5",
"@types/react": "^17.0.11",

@ -13,6 +13,7 @@ import clsx from 'clsx';
import { useIsMobile } from '@/hooks/useIsMobile';
import './Modal.less';
import { stopPropagation } from '@/utils/dom-helper';
const transitionEndListener = (node: HTMLElement, done: () => void) =>
node.addEventListener('transitionend', done, false);
@ -63,10 +64,6 @@ export const Modal: React.FC<ModalProps> = React.memo((props) => {
closeModal();
}, [maskClosable, closeModal]);
const stopPropagation = useCallback((e: React.BaseSyntheticEvent) => {
e.stopPropagation();
}, []);
if (visible === false) {
return null;
}

@ -0,0 +1,147 @@
import { stopPropagation } from '@/utils/dom-helper';
import { Input } from 'antd';
import React, { useCallback, useMemo, useState } from 'react';
import { t } from 'tailchat-shared';
import { PortalAdd, PortalRemove } from './Portal';
import _take from 'lodash/take';
import clsx from 'clsx';
import { useGlobalKeyDown } from '@/hooks/useGlobalKeyDown';
import { isArrowDown, isArrowUp, isEnterHotkey } from '@/utils/hot-key';
import { useHistory } from 'react-router';
let currentQuickSwitcherKey: number | null = null;
interface QuickActionContext {
navigate: (url: string) => void;
}
interface QuickAction {
key: string;
label: string;
action: (context: QuickActionContext) => void;
}
const builtinActions: QuickAction[] = [
{
key: 'personal',
label: t('个人主页'),
action({ navigate }) {
navigate('/main/personal/friends');
},
},
{
key: 'plugins',
label: t('插件中心'),
action({ navigate }) {
navigate('/main/personal/plugins');
},
},
];
function useQuickSwitcherActionContext(): QuickActionContext {
const history = useHistory();
return {
navigate: (url) => {
history.push(url);
},
};
}
const QuickSwitcher: React.FC = React.memo(() => {
const [keyword, setKeyword] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const actionContext = useQuickSwitcherActionContext();
const handleClose = useCallback(() => {
if (!currentQuickSwitcherKey) {
return;
}
PortalRemove(currentQuickSwitcherKey);
currentQuickSwitcherKey = null;
}, []);
const filteredActions = useMemo(() => {
return _take(
builtinActions.filter((action) => action.label.includes(keyword)),
5
);
}, [keyword]);
useGlobalKeyDown((e) => {
if (isArrowUp(e)) {
const newIndex = selectedIndex - 1;
setSelectedIndex(
newIndex >= 0 ? newIndex : filteredActions.length + newIndex
);
} else if (isArrowDown(e)) {
setSelectedIndex((selectedIndex + 1) % filteredActions.length);
}
if (isEnterHotkey(e)) {
const selectedAction = filteredActions[selectedIndex];
typeof selectedAction.action === 'function' &&
selectedAction.action(actionContext);
handleClose();
}
});
return (
<div
className="fixed left-0 right-0 top-0 bottom-0 bg-black bg-opacity-60 flex justify-center"
onClick={handleClose}
>
<div
className="modal-inner bg-content-light dark:bg-content-dark rounded overflow-auto relative p-4"
style={{
marginTop: '20vh',
height: 'fit-content',
maxHeight: '60vh',
width: '60vw',
maxWidth: '1280px',
}}
onClick={stopPropagation}
>
<Input
className="mb-1"
autoFocus={true}
placeholder={t('快速搜索、跳转')}
size="large"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
{filteredActions.map((action, i) => (
<div
key={action.key}
className={clsx('truncate px-2 py-1 rounded', {
'bg-black bg-opacity-20 dark:bg-white dark:bg-opacity-20':
selectedIndex === i,
})}
>
<div className="text-lg">{action.label}</div>
<div
className={clsx('opacity-0 text-gray-400 text-xs', {
'opacity-100': selectedIndex === i,
})}
>
{action.key}
</div>
</div>
))}
</div>
</div>
);
});
QuickSwitcher.displayName = 'QuickSwitcher';
/**
*
*/
export function openQuickSwitcher() {
if (typeof currentQuickSwitcherKey === 'number') {
return;
}
currentQuickSwitcherKey = PortalAdd(<QuickSwitcher />);
}

@ -0,0 +1,22 @@
import { useLayoutEffect } from 'react';
import { useUpdateRef } from 'tailchat-shared';
/**
* keydown hooks
*
*/
export function useGlobalKeyDown(fn: (e: KeyboardEvent) => void) {
const fnRef = useUpdateRef(fn);
useLayoutEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
typeof fnRef.current === 'function' && fnRef.current(e);
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
}

@ -3,9 +3,11 @@ import React from 'react';
import { MainContent } from './Content';
import { Navbar } from './Navbar';
import { MainProvider } from './Provider';
import { useShortcuts } from './useShortcuts';
export const MainRoute: React.FC = React.memo(() => {
useRecordMeasure('AppMainRenderStart');
useShortcuts();
return (
<div className="flex h-full">

@ -0,0 +1,15 @@
import { openQuickSwitcher } from '@/components/QuickSwitcher';
import { useGlobalKeyDown } from '@/hooks/useGlobalKeyDown';
import { isQuickSwitcher } from '@/utils/hot-key';
/**
*
*/
export function useShortcuts() {
useGlobalKeyDown((e) => {
if (isQuickSwitcher(e)) {
// 显示快速开关
openQuickSwitcher();
}
});
}

@ -10,3 +10,10 @@ export function getPopupContainer() {
return document.body;
}
/**
* stopPropagation
*/
export function stopPropagation(e: React.BaseSyntheticEvent) {
e.stopPropagation();
}

@ -1,3 +1,9 @@
import { isHotkey } from 'is-hotkey';
export const isEnterHotkey = isHotkey('enter');
export const isQuickSwitcher = isHotkey('mod+k');
export const isArrowUp = isHotkey('up');
export const isArrowDown = isHotkey('down');

@ -1936,10 +1936,10 @@
dependencies:
"@types/node" "*"
"@types/is-hotkey@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@types/is-hotkey/-/is-hotkey-0.1.3.tgz#1e86be048d3af160e8e676d5cd463f6f7061589a"
integrity sha512-Hz+eHHpMWLBX1CpDXSuQre9nYXN2e2VGVHvkkldxDzo9eFtRpHm5iOlJlZvnNGvele5584cUSkRnFRQb+Wcu0w==
"@types/is-hotkey@^0.1.5":
version "0.1.5"
resolved "https://registry.nlark.com/@types/is-hotkey/download/@types/is-hotkey-0.1.5.tgz#f3123ba21228c0408c10594abf378caddbb802f8"
integrity sha1-8xI7ohIowECMEFlKvzeMrdu4Avg=
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
@ -6375,8 +6375,8 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
is-hotkey@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.2.0.tgz#1835a68171a91e5c9460869d96336947c8340cef"
integrity sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==
resolved "https://registry.npm.taobao.org/is-hotkey/download/is-hotkey-0.2.0.tgz?cache=0&sync_timestamp=1616273570971&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fis-hotkey%2Fdownload%2Fis-hotkey-0.2.0.tgz#1835a68171a91e5c9460869d96336947c8340cef"
integrity sha1-GDWmgXGpHlyUYIadljNpR8g0DO8=
is-interactive@^1.0.0:
version "1.0.0"

Loading…
Cancel
Save