mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 03:25:56 +01:00
Add keyboard shortcuts documentation (#2129)
There is currently no documentation which shortcuts are available to the end user, neither within the application nor the documentation published on scm-manager.org. This PR adds the missing documentation in both places and introduces a new api for developers to add documentation when using `useShortcut`. It also improves the api for conditional shortcuts significantly.
This commit is contained in:
committed by
GitHub
parent
33f7dd994a
commit
65f111b8b4
@@ -7,3 +7,4 @@
|
||||
- /user/profile/
|
||||
- /user/notification/
|
||||
- /user/cli/
|
||||
- /user/shortcuts/
|
||||
|
||||
37
docs/de/user/shortcuts/index.md
Normal file
37
docs/de/user/shortcuts/index.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Tastaturkürzel
|
||||
---
|
||||
Der SCM-Manager unterstützt Tastaturinteraktion und -navigation durch zusätzliche Tastenkürzel.
|
||||
|
||||
### Übersicht
|
||||
|
||||
Während sie den SCM-Manager verwenden, können sie eine Übersicht aller dem aktiven Benutzer auf der aktuellen Seite verfügbaren Tastenkürzel mittels der `?`-Taste aufrufen.
|
||||
|
||||
### Globale Tastenkürzel
|
||||
|
||||
| Key Combination | Description |
|
||||
|-----------------|-------------------------------------|
|
||||
| ? | Öffne die Tastaturkürzelübersicht |
|
||||
| / | Fokussiere die globale Schnellsuche |
|
||||
| alt r | Navigiere zur Repositoryübersicht |
|
||||
| alt u | Navigiere zur Benutzerübersicht |
|
||||
| alt g | Navigiere zur Gruppenübersicht |
|
||||
| alt a | Navigiere zur Administration |
|
||||
|
||||
### Repositoryspezifische Tastenkürzel
|
||||
|
||||
| Key Combination | Description |
|
||||
|-----------------|---------------|
|
||||
| g i | Info |
|
||||
| g b | Branches |
|
||||
| g t | Tags |
|
||||
| g c | Code |
|
||||
| g s | Einstellungen |
|
||||
|
||||
### Tastenkürzel aus Plugin
|
||||
|
||||
Plugins können selbst neue Tastenkürzel definieren.
|
||||
Diese können global oder repository-spezifisch sein oder in einem komplett anderen Kontext angewandt werden.
|
||||
Sie werden automatisch in der Übersicht im SCM-Manager mit aufgelistet.
|
||||
Um die Tastenkürzel eines Plugins innerhalb der Benutzerdokumentation zu finden, verweisen wir hier auf die Dokumentation
|
||||
des jeweiligen Plugins.
|
||||
@@ -16,6 +16,7 @@
|
||||
- /user/profile/
|
||||
- /user/notification/
|
||||
- /user/cli/
|
||||
- /user/shortcuts/
|
||||
|
||||
- section: Administration
|
||||
entries:
|
||||
|
||||
37
docs/en/user/shortcuts/index.md
Normal file
37
docs/en/user/shortcuts/index.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Shortcuts
|
||||
---
|
||||
The SCM-Manager enhances keyboard interaction and navigation through additional shortcuts.
|
||||
|
||||
### Summary
|
||||
|
||||
While using the SCM-Manager, a summary of all shortcuts available to the active user on the current page can be opened
|
||||
from anywhere by pressing the `?` key.
|
||||
|
||||
### Global Shortcuts
|
||||
|
||||
| Key Combination | Description |
|
||||
|-----------------|----------------------------|
|
||||
| ? | Open the shortcut summary |
|
||||
| / | Focus global quick search |
|
||||
| alt r | Navigate to Repositories |
|
||||
| alt u | Navigate to Users |
|
||||
| alt g | Navigate to Groups |
|
||||
| alt a | Navigate to Administration |
|
||||
|
||||
### Repository-specific Shortcuts
|
||||
|
||||
| Key Combination | Description |
|
||||
|-----------------|-------------|
|
||||
| g i | Info |
|
||||
| g b | Branches |
|
||||
| g t | Tags |
|
||||
| g c | Code |
|
||||
| g s | Settings |
|
||||
|
||||
### Plugin Shortcuts
|
||||
|
||||
Plugins can introduce new shortcuts.
|
||||
They may be global, repository-specific or connected to an entirely different context.
|
||||
They will automatically be included in the summary generated within the SCM-Manager.
|
||||
To find the shortcuts outside the SCM-Manager, please refer to the documentation of the plugin.
|
||||
2
gradle/changelog/shortcuts_docs.yaml
Normal file
2
gradle/changelog/shortcuts_docs.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Keyboard shortcuts documentation ([#2129](https://github.com/scm-manager/scm-manager/pull/2129))
|
||||
@@ -27,7 +27,8 @@
|
||||
"string_score": "^0.1.22",
|
||||
"styled-components": "^5.3.5",
|
||||
"systemjs": "0.21.6",
|
||||
"mousetrap": "^1.6.5"
|
||||
"mousetrap": "^1.6.5",
|
||||
"ua-parser-js": "^1.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
@@ -50,6 +51,7 @@
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@types/systemjs": "^0.20.6",
|
||||
"@types/mousetrap": "^1.6.9",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"fetch-mock": "^7.5.1",
|
||||
"react-test-renderer": "^17.0.1"
|
||||
},
|
||||
|
||||
@@ -261,5 +261,23 @@
|
||||
},
|
||||
"tag": {
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"shortcutDocsModal": {
|
||||
"title": "Tastaturkürzel",
|
||||
"description": "Die folgende Tabelle listet alle für Sie auf der aktuellen Seite verfügbaren Tastaturkürzel auf.",
|
||||
"table": {
|
||||
"headers": {
|
||||
"keyCombination": "Tastenkombination",
|
||||
"description": "Beschreibung"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"search": "Fokussiere die globale Schnellsuche",
|
||||
"repositories": "Navigiere zur Repositoryübersicht",
|
||||
"users": "Navigiere zur Benutzerübersicht",
|
||||
"groups": "Navigiere zur Gruppenübersicht",
|
||||
"admin": "Navigiere zur Administration",
|
||||
"docs": "Öffne die Tastaturkürzelübersicht"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,5 +538,12 @@
|
||||
"queryToShort": "Tippe mindestens 2 Zeichen ein, um die Suche zu starten",
|
||||
"emptyResult": "Es wurden keine Ergebnisse für <0>{{query}}</0> gefunden"
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"info": "Info",
|
||||
"branches": "Branches",
|
||||
"tags": "Tags",
|
||||
"code": "Code",
|
||||
"settings": "Einstellungen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,5 +262,23 @@
|
||||
},
|
||||
"tag": {
|
||||
"delete": "Delete"
|
||||
},
|
||||
"shortcutDocsModal": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"description": "The following table lists all keyboard shortcuts available to you on the current page.",
|
||||
"table": {
|
||||
"headers": {
|
||||
"keyCombination": "Key Combination",
|
||||
"description": "Description"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"search": "Focus global quick search",
|
||||
"repositories": "Navigate to Repositories",
|
||||
"users": "Navigate to Users",
|
||||
"groups": "Navigate to Groups",
|
||||
"admin": "Navigate to Administration",
|
||||
"docs": "Open the shortcut summary"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,5 +545,12 @@
|
||||
"queryToShort": "Type at least two characters to start the search",
|
||||
"emptyResult": "Nothing found for query <0>{{query}}</0>"
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"info": "Info",
|
||||
"branches": "Branches",
|
||||
"tags": "Tags",
|
||||
"code": "Code",
|
||||
"settings": "Settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import useShortcut from "../shortcuts/useShortcut";
|
||||
import Login from "./Login";
|
||||
import NavigationBar from "./NavigationBar";
|
||||
import styled from "styled-components";
|
||||
import ShortcutDocsModal from "../shortcuts/ShortcutDocsModal";
|
||||
|
||||
const AppWrapper = styled.div`
|
||||
min-height: 100vh;
|
||||
@@ -47,25 +48,18 @@ const App: FC = () => {
|
||||
usePauseShortcutsWhenModalsActive();
|
||||
|
||||
const history = useHistory();
|
||||
useShortcut("option+r", () => {
|
||||
if (index && index._links["repositories"]) {
|
||||
history.push("/repos/");
|
||||
}
|
||||
|
||||
useShortcut("option+r", () => history.push("/repos/"), t("shortcuts.repositories"), {
|
||||
active: !!index?._links["repositories"],
|
||||
});
|
||||
useShortcut("option+u", () => {
|
||||
if (index && index._links["users"]) {
|
||||
history.push("/users/");
|
||||
}
|
||||
useShortcut("option+u", () => history.push("/users/"), t("shortcuts.users"), {
|
||||
active: !!index?._links["users"],
|
||||
});
|
||||
useShortcut("option+g", () => {
|
||||
if (index && index._links["groups"]) {
|
||||
history.push("/groups/");
|
||||
}
|
||||
useShortcut("option+g", () => history.push("/groups/"), t("shortcuts.groups"), {
|
||||
active: !!index?._links["groups"],
|
||||
});
|
||||
useShortcut("option+a", () => {
|
||||
if (index && index._links["config"]) {
|
||||
history.push("/admin/");
|
||||
}
|
||||
useShortcut("option+a", () => history.push("/admin/"), t("shortcuts.admin"), {
|
||||
active: !!index?._links["config"],
|
||||
});
|
||||
|
||||
if (!index) {
|
||||
@@ -95,6 +89,7 @@ const App: FC = () => {
|
||||
<Header authenticated={authenticatedOrAnonymous}>
|
||||
<NavigationBar links={index._links} />
|
||||
</Header>
|
||||
<ShortcutDocsModal />
|
||||
<div className="is-flex-grow-1">{content}</div>
|
||||
<Footer me={me} version={index.version} links={index._links} />
|
||||
</AppWrapper>
|
||||
|
||||
@@ -33,6 +33,7 @@ import i18next from "i18next";
|
||||
import { binder, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import InitializationAdminAccountStep from "./InitializationAdminAccountStep";
|
||||
import InitializationPluginWizardStep from "./InitializationPluginWizardStep";
|
||||
import { ShortcutDocsContextProvider } from "../shortcuts/useShortcutDocs";
|
||||
|
||||
const Index: FC = () => {
|
||||
const { isLoading, error, data } = useIndex();
|
||||
@@ -60,6 +61,7 @@ const Index: FC = () => {
|
||||
return (
|
||||
<ErrorBoundary fallback={IndexErrorPage}>
|
||||
<ScrollToTop>
|
||||
<ShortcutDocsContextProvider>
|
||||
<ActiveModalCountContextProvider>
|
||||
<NamespaceAndNameContextProvider>
|
||||
<PluginLoader link={link} loaded={pluginsLoaded} callback={() => setPluginsLoaded(true)}>
|
||||
@@ -67,6 +69,7 @@ const Index: FC = () => {
|
||||
</PluginLoader>
|
||||
</NamespaceAndNameContextProvider>
|
||||
</ActiveModalCountContextProvider>
|
||||
</ShortcutDocsContextProvider>
|
||||
</ScrollToTop>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,8 @@ import React, {
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo, useRef,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Hit, Links, Repository, ValueHitField } from "@scm-manager/ui-types";
|
||||
@@ -357,7 +358,7 @@ const OmniSearch: FC = () => {
|
||||
searchTypes.sort(orderTypes(t));
|
||||
|
||||
const id = useCallback(namespaceAndName, []);
|
||||
useShortcut("/", () => searchInputRef.current?.focus());
|
||||
useShortcut("/", () => searchInputRef.current?.focus(), t("shortcuts.search"));
|
||||
|
||||
const entries = useMemo(() => {
|
||||
const newEntries = [];
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { match as Match } from "react-router";
|
||||
import { Link as RouteLink, Redirect, Route, RouteProps, Switch, useHistory, useRouteMatch } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -101,27 +101,27 @@ const RepositoryRoot = () => {
|
||||
|
||||
const url = urls.matchedUrlFromMatch(match);
|
||||
|
||||
useShortcut("g i", () => {
|
||||
history.push(`${url}/info`);
|
||||
});
|
||||
useShortcut("g b", () => {
|
||||
if (repository && repository._links["branches"]) {
|
||||
history.push(`${url}/branches/`);
|
||||
const codeLinkname = useMemo(() => {
|
||||
if (repository?._links?.sources) {
|
||||
return "sources";
|
||||
}
|
||||
});
|
||||
useShortcut("g t", () => {
|
||||
if (repository && repository._links["tags"]) {
|
||||
history.push(`${url}/tags/`);
|
||||
if (repository?._links?.changesets) {
|
||||
return "changesets";
|
||||
}
|
||||
return "";
|
||||
}, [repository]);
|
||||
|
||||
useShortcut("g i", () => history.push(`${url}/info`), t("shortcuts.info"));
|
||||
useShortcut("g b", () => history.push(`${url}/branches/`), t("shortcuts.branches"), {
|
||||
active: !!repository?._links["branches"],
|
||||
});
|
||||
useShortcut("g c", () => {
|
||||
if (repository && repository._links[getCodeLinkname()]) {
|
||||
history.push(evaluateDestinationForCodeLink());
|
||||
}
|
||||
useShortcut("g t", () => history.push(`${url}/tags/`), t("shortcuts.tags"), {
|
||||
active: !!repository?._links["tags"],
|
||||
});
|
||||
useShortcut("g s", () => {
|
||||
history.push(`${url}/settings/general`);
|
||||
useShortcut("g c", () => history.push(evaluateDestinationForCodeLink()), t("shortcuts.code"), {
|
||||
active: !!repository?._links[codeLinkname],
|
||||
});
|
||||
useShortcut("g s", () => history.push(`${url}/settings/general`), t("shortcuts.settings"));
|
||||
|
||||
useEffect(() => {
|
||||
if (repository) {
|
||||
@@ -239,16 +239,6 @@ const RepositoryRoot = () => {
|
||||
return !!route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
const getCodeLinkname = () => {
|
||||
if (repository?._links?.sources) {
|
||||
return "sources";
|
||||
}
|
||||
if (repository?._links?.changesets) {
|
||||
return "changesets";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const evaluateDestinationForCodeLink = () => {
|
||||
if (repository?._links?.sources) {
|
||||
return `${url}/code/sources/`;
|
||||
@@ -381,7 +371,7 @@ const RepositoryRoot = () => {
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName={getCodeLinkname()}
|
||||
linkName={codeLinkname}
|
||||
to={evaluateDestinationForCodeLink()}
|
||||
icon="fas fa-code"
|
||||
label={t("repositoryRoot.menu.sourcesNavLink")}
|
||||
|
||||
68
scm-ui/ui-webapp/src/shortcuts/ShortcutDocsModal.tsx
Normal file
68
scm-ui/ui-webapp/src/shortcuts/ShortcutDocsModal.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import useShortcutDocs from "./useShortcutDocs";
|
||||
import { Column, Modal, Table } from "@scm-manager/ui-components";
|
||||
import useShortcut from "./useShortcut";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import splitKeyCombination from "./splitKeyCombination";
|
||||
|
||||
const keyComparator = ([a]: [string, string], [b]: [string, string]) => (a > b ? 1 : -1);
|
||||
|
||||
const ShortcutDocsModal = () => {
|
||||
const { docs } = useShortcutDocs();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [t] = useTranslation("commons");
|
||||
useShortcut("?", () => setOpen(true), t("shortcuts.docs"));
|
||||
|
||||
return (
|
||||
<Modal title={t("shortcutDocsModal.title")} closeFunction={() => setOpen(false)} active={open}>
|
||||
<p className="mb-2">{t("shortcutDocsModal.description")}</p>
|
||||
<Table data={Object.entries(docs).sort(keyComparator)}>
|
||||
<Column header={t("shortcutDocsModal.table.headers.keyCombination")}>
|
||||
{([key]: [string, string]) =>
|
||||
splitKeyCombination(key).map((k, i) => (
|
||||
<span
|
||||
className={classNames(
|
||||
"has-background-secondary-less has-text-secondary-most py-1 px-2 has-rounded-border",
|
||||
{
|
||||
"ml-1": i > 0,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{k}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</Column>
|
||||
<Column header={t("shortcutDocsModal.table.headers.description")}>
|
||||
{([, description]: [string, string]) => description}
|
||||
</Column>
|
||||
</Table>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShortcutDocsModal;
|
||||
56
scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.test.ts
Normal file
56
scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import splitKeyCombination from "./splitKeyCombination";
|
||||
|
||||
const MAC_USER_AGENT =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12";
|
||||
const WINDOWS_USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36";
|
||||
const LINUX_USER_AGENT =
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36";
|
||||
|
||||
describe("splitKeyCombination", () => {
|
||||
it("should split and replace correctly", () => {
|
||||
expect(splitKeyCombination("alt+a meta+b meta+c", WINDOWS_USER_AGENT)).toEqual([
|
||||
"alt",
|
||||
"a",
|
||||
"meta",
|
||||
"b",
|
||||
"meta",
|
||||
"c",
|
||||
]);
|
||||
expect(splitKeyCombination("option+a command+b command+c mod+d", LINUX_USER_AGENT)).toEqual([
|
||||
"alt",
|
||||
"a",
|
||||
"meta",
|
||||
"b",
|
||||
"meta",
|
||||
"c",
|
||||
"ctrl",
|
||||
"d",
|
||||
]);
|
||||
expect(splitKeyCombination("alt+a meta+b mod+c", MAC_USER_AGENT)).toEqual(["option", "a", "⌘", "b", "⌘", "c"]);
|
||||
});
|
||||
});
|
||||
37
scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.ts
Normal file
37
scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import parser from "ua-parser-js";
|
||||
|
||||
export default function splitKeyCombination(key: string, userAgent = window.navigator.userAgent) {
|
||||
const {
|
||||
os: { name: osName },
|
||||
} = parser(userAgent);
|
||||
const isMacOS = osName === "Mac OS";
|
||||
return key
|
||||
.replace(/(option|alt)/g, isMacOS ? "option" : "alt")
|
||||
.replace(/(command|meta)/g, isMacOS ? "⌘" : "meta")
|
||||
.replace("mod", isMacOS ? "⌘" : "ctrl")
|
||||
.split(/[+ ]/);
|
||||
}
|
||||
@@ -24,6 +24,16 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Mousetrap from "mousetrap";
|
||||
import useShortcutDocs from "./useShortcutDocs";
|
||||
|
||||
export type UseShortcutOptions = {
|
||||
/**
|
||||
* Whether the shortcut is currently active
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* ## Summary
|
||||
@@ -46,17 +56,31 @@ import Mousetrap from "mousetrap";
|
||||
*
|
||||
* ## Combinations
|
||||
*
|
||||
* Keys can be combined with the "+" separator, but without extra whitespaces.
|
||||
* Keys can be combined by separating them with a whitespace.
|
||||
* For using modifiers, prefix the key with the modifier and concat them with a "+".
|
||||
*
|
||||
* @param key
|
||||
* @param callback
|
||||
* @example useShortcut("/", ...)
|
||||
* Please also refer to the examples.
|
||||
*
|
||||
* @param key The keycode combination that triggers the callback
|
||||
* @param callback The function that is executed when the key combination is pressed
|
||||
* @param description The translated description used for the shortcut documentation
|
||||
* @param options Whether the shortcut is currently active, defaults to true
|
||||
* @example useShortcut("a b", ...)
|
||||
* @example useShortcut("ctrl+shift+k", ...)
|
||||
* @see https://github.com/ccampbell/mousetrap
|
||||
* @see https://craig.is/killing/mice
|
||||
*/
|
||||
export default function useShortcut(key: string, callback: (e: KeyboardEvent) => void) {
|
||||
export default function useShortcut(
|
||||
key: string,
|
||||
callback: (e: KeyboardEvent) => void,
|
||||
description: string,
|
||||
options?: UseShortcutOptions
|
||||
) {
|
||||
const { add, remove } = useShortcutDocs();
|
||||
useEffect(() => {
|
||||
const active = !options || options.active === undefined || options.active;
|
||||
if (active) {
|
||||
add(key, description);
|
||||
Mousetrap.bind(key, (e) => {
|
||||
callback(e);
|
||||
/*
|
||||
@@ -66,9 +90,11 @@ export default function useShortcut(key: string, callback: (e: KeyboardEvent) =>
|
||||
*/
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
remove(key);
|
||||
Mousetrap.unbind(key);
|
||||
};
|
||||
}, [key, callback]);
|
||||
}, [key, callback, add, remove, options, description]);
|
||||
}
|
||||
|
||||
53
scm-ui/ui-webapp/src/shortcuts/useShortcutDocs.tsx
Normal file
53
scm-ui/ui-webapp/src/shortcuts/useShortcutDocs.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC, useContext, useMemo, useRef } from "react";
|
||||
|
||||
export type ShortcutDocsContextType = {
|
||||
docs: Readonly<Record<string, string>>;
|
||||
add: (key: string, description: string) => void;
|
||||
remove: (key: string) => void;
|
||||
};
|
||||
|
||||
const ShortcutDocsContext = React.createContext<ShortcutDocsContextType>({} as ShortcutDocsContextType);
|
||||
|
||||
export const ShortcutDocsContextProvider: FC = ({ children }) => {
|
||||
const docs = useRef<Record<string, string>>({});
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
docs: docs.current,
|
||||
add: (key: string, description: string) => (docs.current[key] = description),
|
||||
remove: (key: string) => {
|
||||
delete docs.current[key];
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return <ShortcutDocsContext.Provider value={value}>{children}</ShortcutDocsContext.Provider>;
|
||||
};
|
||||
|
||||
export default function useShortcutDocs() {
|
||||
return useContext(ShortcutDocsContext);
|
||||
}
|
||||
10
yarn.lock
10
yarn.lock
@@ -4169,6 +4169,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/to-camel-case/-/to-camel-case-1.0.0.tgz#927ef0a7294d90b1835466c29b64b8ad2a32d8b5"
|
||||
integrity sha512-LXJOP0xvOUB4dKu+t7EVhSsM2NauLSZSOGkBS7Wqz3lWHIseCJnMDG+HrZHLFZQ39Fq3jr4RErJyQzfsoOlXSA==
|
||||
|
||||
"@types/ua-parser-js@^0.7.36":
|
||||
version "0.7.36"
|
||||
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190"
|
||||
integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==
|
||||
|
||||
"@types/uglify-js@*":
|
||||
version "3.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.16.0.tgz#2cf74a0e6ebb6cd54c0d48e509d5bd91160a9602"
|
||||
@@ -18717,6 +18722,11 @@ typescript@^4.0.5, typescript@^4.6.0:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||
|
||||
ua-parser-js@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
|
||||
integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.16.3"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.3.tgz#94c7a63337ee31227a18d03b8a3041c210fd1f1d"
|
||||
|
||||
Reference in New Issue
Block a user