mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
Merge pull request #1146 from hgiesel/shortcuts
Shortcut API for Editor
This commit is contained in:
commit
02ebab7491
20 changed files with 469 additions and 152 deletions
|
@ -2,14 +2,14 @@ editing-add-media = Add Media
|
||||||
editing-align-left = Align left
|
editing-align-left = Align left
|
||||||
editing-align-right = Align right
|
editing-align-right = Align right
|
||||||
editing-an-error-occurred-while-opening = An error occurred while opening { $val }
|
editing-an-error-occurred-while-opening = An error occurred while opening { $val }
|
||||||
editing-attach-picturesaudiovideo-f3 = Attach pictures/audio/video (F3)
|
editing-attach-picturesaudiovideo = Attach pictures/audio/video
|
||||||
editing-bold-text-ctrlandb = Bold text (Ctrl+B)
|
editing-bold-text = Bold text
|
||||||
editing-cards = Cards
|
editing-cards = Cards
|
||||||
editing-center = Center
|
editing-center = Center
|
||||||
editing-change-colour-f8 = Change colour (F8)
|
editing-change-color = Change color
|
||||||
editing-cloze-deletion-ctrlandshiftandc = Cloze deletion (Ctrl+Shift+C)
|
editing-cloze-deletion = Cloze deletion
|
||||||
editing-couldnt-record-audio-have-you-installed = Couldn't record audio. Have you installed 'lame'?
|
editing-couldnt-record-audio-have-you-installed = Couldn't record audio. Have you installed 'lame'?
|
||||||
editing-customize-card-templates-ctrlandl = Customize Card Templates (Ctrl+L)
|
editing-customize-card-templates = Customize Card Templates
|
||||||
editing-customize-fields = Customize Fields
|
editing-customize-fields = Customize Fields
|
||||||
editing-cut = Cut
|
editing-cut = Cut
|
||||||
editing-edit-current = Edit Current
|
editing-edit-current = Edit Current
|
||||||
|
@ -17,7 +17,7 @@ editing-edit-html = Edit HTML
|
||||||
editing-fields = Fields
|
editing-fields = Fields
|
||||||
editing-html-editor = HTML Editor
|
editing-html-editor = HTML Editor
|
||||||
editing-indent = Increase indent
|
editing-indent = Increase indent
|
||||||
editing-italic-text-ctrlandi = Italic text (Ctrl+I)
|
editing-italic-text = Italic text
|
||||||
editing-jump-to-tags-with-ctrlandshiftandt = Jump to tags with Ctrl+Shift+T
|
editing-jump-to-tags-with-ctrlandshiftandt = Jump to tags with Ctrl+Shift+T
|
||||||
editing-justify = Justify
|
editing-justify = Justify
|
||||||
editing-latex = LaTeX
|
editing-latex = LaTeX
|
||||||
|
@ -30,14 +30,29 @@ editing-media = Media
|
||||||
editing-ordered-list = Ordered list
|
editing-ordered-list = Ordered list
|
||||||
editing-outdent = Decrease indent
|
editing-outdent = Decrease indent
|
||||||
editing-paste = Paste
|
editing-paste = Paste
|
||||||
editing-record-audio-f5 = Record audio (F5)
|
editing-record-audio = Record audio
|
||||||
editing-remove-formatting-ctrlandr = Remove formatting (Ctrl+R)
|
editing-remove-formatting = Remove formatting
|
||||||
editing-set-foreground-colour-f7 = Set foreground colour (F7)
|
editing-set-foreground-color = Set foreground color
|
||||||
editing-show-duplicates = Show Duplicates
|
editing-show-duplicates = Show Duplicates
|
||||||
editing-subscript-ctrland = Subscript (Ctrl+=)
|
editing-subscript = Subscript
|
||||||
editing-superscript-ctrlandand = Superscript (Ctrl++)
|
editing-superscript = Superscript
|
||||||
editing-tags = Tags
|
editing-tags = Tags
|
||||||
editing-to-make-a-cloze-deletion-on = To make a cloze deletion on an existing note, you need to change it to a cloze type first, via 'Notes>Change Note Type'
|
editing-to-make-a-cloze-deletion-on = To make a cloze deletion on an existing note, you need to change it to a cloze type first, via 'Notes>Change Note Type'
|
||||||
editing-underline-text-ctrlandu = Underline text (Ctrl+U)
|
editing-underline-text = Underline text
|
||||||
editing-unordered-list = Unordered list
|
editing-unordered-list = Unordered list
|
||||||
editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will not work until you switch the type at the top to Cloze.
|
editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will not work until you switch the type at the top to Cloze.
|
||||||
|
|
||||||
|
## deprecated, do not use
|
||||||
|
|
||||||
|
editing-bold-text-ctrlandb = Bold text (Ctrl+B)
|
||||||
|
editing-italic-text-ctrlandi = Italic text (Ctrl+I)
|
||||||
|
editing-underline-text-ctrlandu = Underline text (Ctrl+U)
|
||||||
|
editing-subscript-ctrland = Subscript (Ctrl+=)
|
||||||
|
editing-superscript-ctrlandand = Superscript (Ctrl++)
|
||||||
|
editing-remove-formatting-ctrlandr = Remove formatting (Ctrl+R)
|
||||||
|
editing-record-audio-f5 = Record audio (F5)
|
||||||
|
editing-attach-picturesaudiovideo-f3 = Attach pictures/audio/video (F3)
|
||||||
|
editing-cloze-deletion-ctrlandshiftandc = Cloze deletion (Ctrl+Shift+C)
|
||||||
|
editing-change-colour-f8 = Change colour (F8)
|
||||||
|
editing-set-foreground-colour-f7 = Set foreground colour (F7)
|
||||||
|
editing-customize-card-templates-ctrlandl = Customize Card Templates (Ctrl+L)
|
||||||
|
|
2
ftl/core/keyboard.ftl
Normal file
2
ftl/core/keyboard.ftl
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
keyboard-ctrl = Ctrl
|
||||||
|
keyboard-shift = Shift
|
|
@ -311,26 +311,6 @@ $editorToolbar.addButtonGroup({{
|
||||||
def setupShortcuts(self) -> None:
|
def setupShortcuts(self) -> None:
|
||||||
# if a third element is provided, enable shortcut even when no field selected
|
# if a third element is provided, enable shortcut even when no field selected
|
||||||
cuts: List[Tuple] = [
|
cuts: List[Tuple] = [
|
||||||
("Ctrl+L", self.onCardLayout, True),
|
|
||||||
("Ctrl+B", self.toggleBold),
|
|
||||||
("Ctrl+I", self.toggleItalic),
|
|
||||||
("Ctrl+U", self.toggleUnderline),
|
|
||||||
("Ctrl++", self.toggleSuper),
|
|
||||||
("Ctrl+=", self.toggleSub),
|
|
||||||
("Ctrl+R", self.removeFormat),
|
|
||||||
("F7", self.onForeground),
|
|
||||||
("F8", self.onChangeCol),
|
|
||||||
("Ctrl+Shift+C", self.onCloze),
|
|
||||||
("Ctrl+Shift+Alt+C", self.onCloze),
|
|
||||||
("F3", self.onAddMedia),
|
|
||||||
("F5", self.onRecSound),
|
|
||||||
("Ctrl+T, T", self.insertLatex),
|
|
||||||
("Ctrl+T, E", self.insertLatexEqn),
|
|
||||||
("Ctrl+T, M", self.insertLatexMathEnv),
|
|
||||||
("Ctrl+M, M", self.insertMathjaxInline),
|
|
||||||
("Ctrl+M, E", self.insertMathjaxBlock),
|
|
||||||
("Ctrl+M, C", self.insertMathjaxChemistry),
|
|
||||||
("Ctrl+Shift+X", self.onHtmlEdit),
|
|
||||||
("Ctrl+Shift+T", self.onFocusTags, True),
|
("Ctrl+Shift+T", self.onFocusTags, True),
|
||||||
]
|
]
|
||||||
gui_hooks.editor_did_init_shortcuts(cuts, self)
|
gui_hooks.editor_did_init_shortcuts(cuts, self)
|
||||||
|
|
|
@ -3,12 +3,16 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { getContext } from "svelte";
|
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
import { nightModeKey } from "./contextKeys";
|
import { nightModeKey } from "./contextKeys";
|
||||||
|
import { mergeTooltipAndShortcut } from "./helpers";
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let className = "";
|
export let className = "";
|
||||||
export let tooltip: string;
|
export let tooltip: string | undefined;
|
||||||
|
export let shortcutLabel: string | undefined;
|
||||||
|
|
||||||
|
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
|
||||||
|
|
||||||
export let onChange: (event: Event) => void;
|
export let onChange: (event: Event) => void;
|
||||||
|
|
||||||
|
@ -18,11 +22,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
const nightMode = getContext(nightModeKey);
|
const nightMode = getContext(nightModeKey);
|
||||||
|
|
||||||
|
let buttonRef: HTMLButtonElement;
|
||||||
let inputRef: HTMLInputElement;
|
let inputRef: HTMLInputElement;
|
||||||
|
|
||||||
function delegateToInput() {
|
function delegateToInput() {
|
||||||
inputRef.click();
|
inputRef.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -57,13 +65,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
bind:this={buttonRef}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
{id}
|
{id}
|
||||||
class={extendClassName(className)}
|
class={extendClassName(className)}
|
||||||
class:btn-day={!nightMode}
|
class:btn-day={!nightMode}
|
||||||
class:btn-night={nightMode}
|
class:btn-night={nightMode}
|
||||||
title={tooltip}
|
{title}
|
||||||
on:click={delegateToInput}
|
on:click={delegateToInput}
|
||||||
on:mousedown|preventDefault>
|
on:mousedown|preventDefault>
|
||||||
<input bind:this={inputRef} type="color" on:change={onChange} />
|
<input tabindex="-1" bind:this={inputRef} type="color" on:change={onChange} />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -48,6 +48,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let className = "";
|
export let className = "";
|
||||||
export let tooltip: string;
|
export let tooltip: string;
|
||||||
|
export let shortcutLabel: string | undefined;
|
||||||
export let icon: string;
|
export let icon: string;
|
||||||
|
|
||||||
export let command: string;
|
export let command: string;
|
||||||
|
@ -80,6 +81,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
{id}
|
{id}
|
||||||
{className}
|
{className}
|
||||||
{tooltip}
|
{tooltip}
|
||||||
|
{shortcutLabel}
|
||||||
{active}
|
{active}
|
||||||
{disables}
|
{disables}
|
||||||
{dropdownToggle}
|
{dropdownToggle}
|
||||||
|
|
|
@ -3,18 +3,23 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { getContext } from "svelte";
|
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
import { nightModeKey } from "./contextKeys";
|
import { nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let className = "";
|
export let className = "";
|
||||||
export let tooltip: string;
|
export let tooltip: string;
|
||||||
|
export let label: string;
|
||||||
|
export let shortcutLabel: string | undefined;
|
||||||
|
|
||||||
export let onClick: (event: MouseEvent) => void;
|
export let onClick: (event: MouseEvent) => void;
|
||||||
export let label: string;
|
|
||||||
export let endLabel: string;
|
let buttonRef: HTMLButtonElement;
|
||||||
|
|
||||||
const nightMode = getContext(nightModeKey);
|
const nightMode = getContext(nightModeKey);
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -60,12 +65,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<button
|
<button
|
||||||
{id}
|
{id}
|
||||||
|
bind:this={buttonRef}
|
||||||
class={`btn dropdown-item ${className}`}
|
class={`btn dropdown-item ${className}`}
|
||||||
class:btn-day={!nightMode}
|
class:btn-day={!nightMode}
|
||||||
class:btn-night={nightMode}
|
class:btn-night={nightMode}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
on:mousedown|preventDefault>
|
on:mousedown|preventDefault>
|
||||||
<span class:me-3={endLabel}>{label}</span>
|
<span class:me-3={shortcutLabel}>{label}</span>
|
||||||
{#if endLabel}<span class="monospace">{endLabel}</span>{/if}
|
{#if shortcutLabel}<span class="monospace">{shortcutLabel}</span>{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -7,7 +7,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let className = "";
|
export let className = "";
|
||||||
export let tooltip: string;
|
export let tooltip: string | undefined;
|
||||||
|
export let shortcutLabel: string | undefined;
|
||||||
export let icon: string;
|
export let icon: string;
|
||||||
|
|
||||||
export let onClick: (event: MouseEvent) => void;
|
export let onClick: (event: MouseEvent) => void;
|
||||||
|
@ -16,6 +17,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let dropdownToggle = false;
|
export let dropdownToggle = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SquareButton {id} {className} {tooltip} {onClick} {disables} {dropdownToggle} on:mount>
|
<SquareButton
|
||||||
|
{id}
|
||||||
|
{className}
|
||||||
|
{tooltip}
|
||||||
|
{shortcutLabel}
|
||||||
|
{onClick}
|
||||||
|
{disables}
|
||||||
|
{dropdownToggle}
|
||||||
|
on:mount>
|
||||||
{@html icon}
|
{@html icon}
|
||||||
</SquareButton>
|
</SquareButton>
|
||||||
|
|
|
@ -6,12 +6,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { Readable } from "svelte/store";
|
import type { Readable } from "svelte/store";
|
||||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
import { disabledKey, nightModeKey } from "./contextKeys";
|
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||||
|
import { mergeTooltipAndShortcut } from "./helpers";
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let className = "";
|
export let className = "";
|
||||||
|
export let tooltip: string | undefined;
|
||||||
|
export let shortcutLabel: string | undefined;
|
||||||
export let label: string;
|
export let label: string;
|
||||||
export let tooltip: string;
|
|
||||||
|
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
|
||||||
|
|
||||||
export let onClick: (event: MouseEvent) => void;
|
export let onClick: (event: MouseEvent) => void;
|
||||||
export let disables = true;
|
export let disables = true;
|
||||||
export let dropdownToggle = false;
|
export let dropdownToggle = false;
|
||||||
|
@ -61,7 +65,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
class:btn-night={nightMode}
|
class:btn-night={nightMode}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
disabled={_disabled}
|
disabled={_disabled}
|
||||||
title={tooltip}
|
{title}
|
||||||
{...extraProps}
|
{...extraProps}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
on:mousedown|preventDefault>
|
on:mousedown|preventDefault>
|
||||||
|
|
|
@ -6,10 +6,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { Readable } from "svelte/store";
|
import type { Readable } from "svelte/store";
|
||||||
import { getContext, onMount, createEventDispatcher } from "svelte";
|
import { getContext, onMount, createEventDispatcher } from "svelte";
|
||||||
import { disabledKey, nightModeKey } from "./contextKeys";
|
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||||
|
import { mergeTooltipAndShortcut } from "./helpers";
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let className = "";
|
export let className = "";
|
||||||
export let tooltip: string;
|
export let tooltip: string | undefined;
|
||||||
|
export let shortcutLabel: string | undefined;
|
||||||
|
|
||||||
|
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
|
||||||
|
|
||||||
export let onClick: (event: MouseEvent) => void;
|
export let onClick: (event: MouseEvent) => void;
|
||||||
export let active = false;
|
export let active = false;
|
||||||
|
@ -79,7 +83,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
class:btn-day={!nightMode}
|
class:btn-day={!nightMode}
|
||||||
class:btn-night={nightMode}
|
class:btn-night={nightMode}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
title={tooltip}
|
{title}
|
||||||
disabled={_disabled}
|
disabled={_disabled}
|
||||||
{...extraProps}
|
{...extraProps}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
|
|
9
ts/editor-toolbar/WithShortcut.d.ts
vendored
Normal file
9
ts/editor-toolbar/WithShortcut.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
|
||||||
|
export interface WithShortcutProps {
|
||||||
|
button: ToolbarItem;
|
||||||
|
shortcut: string;
|
||||||
|
optionalModifiers: string[];
|
||||||
|
}
|
46
ts/editor-toolbar/WithShortcut.svelte
Normal file
46
ts/editor-toolbar/WithShortcut.svelte
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
import type { Modifier } from "anki/shortcuts";
|
||||||
|
|
||||||
|
import { onDestroy } from "svelte";
|
||||||
|
import { registerShortcut, getPlatformString } from "anki/shortcuts";
|
||||||
|
|
||||||
|
export let button: ToolbarItem;
|
||||||
|
export let shortcut: string;
|
||||||
|
export let optionalModifiers: Modifier[];
|
||||||
|
|
||||||
|
function extend({ ...rest }: DynamicSvelteComponent): DynamicSvelteComponent {
|
||||||
|
const shortcutLabel = getPlatformString(shortcut);
|
||||||
|
|
||||||
|
return {
|
||||||
|
shortcutLabel,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let deregister: () => void;
|
||||||
|
|
||||||
|
function createShortcut({ detail }: CustomEvent): void {
|
||||||
|
const mounted: HTMLButtonElement = detail.button;
|
||||||
|
deregister = registerShortcut(
|
||||||
|
(event: KeyboardEvent) => {
|
||||||
|
mounted.dispatchEvent(new KeyboardEvent("click", event));
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
shortcut,
|
||||||
|
optionalModifiers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => deregister());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component
|
||||||
|
this={button.component}
|
||||||
|
{...extend(button)}
|
||||||
|
on:mount={createShortcut} />
|
|
@ -20,6 +20,9 @@ import type { DropdownItemProps } from "./DropdownItem";
|
||||||
import WithDropdownMenu from "./WithDropdownMenu.svelte";
|
import WithDropdownMenu from "./WithDropdownMenu.svelte";
|
||||||
import type { WithDropdownMenuProps } from "./WithDropdownMenu";
|
import type { WithDropdownMenuProps } from "./WithDropdownMenu";
|
||||||
|
|
||||||
|
import WithShortcut from "./WithShortcut.svelte";
|
||||||
|
import type { WithShortcutProps } from "./WithShortcut";
|
||||||
|
|
||||||
import { dynamicComponent } from "sveltelib/dynamicComponent";
|
import { dynamicComponent } from "sveltelib/dynamicComponent";
|
||||||
|
|
||||||
export const labelButton = dynamicComponent<typeof LabelButton, LabelButtonProps>(
|
export const labelButton = dynamicComponent<typeof LabelButton, LabelButtonProps>(
|
||||||
|
@ -55,3 +58,7 @@ export const withDropdownMenu = dynamicComponent<
|
||||||
typeof WithDropdownMenu,
|
typeof WithDropdownMenu,
|
||||||
WithDropdownMenuProps
|
WithDropdownMenuProps
|
||||||
>(WithDropdownMenu);
|
>(WithDropdownMenu);
|
||||||
|
|
||||||
|
export const withShortcut = dynamicComponent<typeof WithShortcut, WithShortcutProps>(
|
||||||
|
WithShortcut
|
||||||
|
);
|
||||||
|
|
14
ts/editor-toolbar/helpers.ts
Normal file
14
ts/editor-toolbar/helpers.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export function mergeTooltipAndShortcut(
|
||||||
|
tooltip: string | undefined,
|
||||||
|
shortcutLabel: string | undefined
|
||||||
|
): string | undefined {
|
||||||
|
return tooltip
|
||||||
|
? shortcutLabel
|
||||||
|
? `${tooltip} (${shortcutLabel})`
|
||||||
|
: tooltip
|
||||||
|
: shortcutLabel
|
||||||
|
? `(${shortcutLabel})`
|
||||||
|
: undefined;
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type IconButton from "editor-toolbar/IconButton.svelte";
|
import type WithShortcut from "editor-toolbar/WithShortcut.svelte";
|
||||||
import type { IconButtonProps } from "editor-toolbar/IconButton";
|
import type { WithShortcutProps } from "editor-toolbar/WithShortcut";
|
||||||
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||||
|
|
||||||
import * as tr from "anki/i18n";
|
import * as tr from "anki/i18n";
|
||||||
import { iconButton } from "editor-toolbar/dynamicComponents";
|
import { iconButton, withShortcut } from "editor-toolbar/dynamicComponents";
|
||||||
|
|
||||||
import bracketsIcon from "./code-brackets.svg";
|
import bracketsIcon from "./code-brackets.svg";
|
||||||
|
|
||||||
|
@ -35,17 +35,21 @@ function getCurrentHighestCloze(increment: boolean): number {
|
||||||
return Math.max(1, highest);
|
return Math.max(1, highest);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCloze(event: MouseEvent): void {
|
function onCloze(event: KeyboardEvent | MouseEvent): void {
|
||||||
const highestCloze = getCurrentHighestCloze(!event.altKey);
|
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
|
||||||
wrap(`{{c${highestCloze}::`, "}}");
|
wrap(`{{c${highestCloze}::`, "}}");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClozeButton(): DynamicSvelteComponent<typeof IconButton> &
|
export function getClozeButton(): DynamicSvelteComponent<typeof WithShortcut> &
|
||||||
IconButtonProps {
|
WithShortcutProps {
|
||||||
return iconButton({
|
return withShortcut({
|
||||||
id: "cloze",
|
id: "cloze",
|
||||||
|
shortcut: "Control+Shift+KeyC",
|
||||||
|
optionalModifiers: ["Alt"],
|
||||||
|
button: iconButton({
|
||||||
icon: bracketsIcon,
|
icon: bracketsIcon,
|
||||||
onClick: onCloze,
|
onClick: onCloze,
|
||||||
tooltip: tr.editingClozeDeletionCtrlandshiftandc(),
|
tooltip: tr.editingClozeDeletion(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,12 @@ import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte";
|
||||||
import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
|
import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
|
||||||
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||||
|
|
||||||
import { iconButton, colorPicker, buttonGroup } from "editor-toolbar/dynamicComponents";
|
import {
|
||||||
|
iconButton,
|
||||||
|
colorPicker,
|
||||||
|
buttonGroup,
|
||||||
|
withShortcut,
|
||||||
|
} from "editor-toolbar/dynamicComponents";
|
||||||
import * as tr from "anki/i18n";
|
import * as tr from "anki/i18n";
|
||||||
|
|
||||||
import squareFillIcon from "./square-fill.svg";
|
import squareFillIcon from "./square-fill.svg";
|
||||||
|
@ -26,17 +31,23 @@ function wrapWithForecolor(color: string): void {
|
||||||
|
|
||||||
export function getColorGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
export function getColorGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
ButtonGroupProps {
|
ButtonGroupProps {
|
||||||
const forecolorButton = iconButton({
|
const forecolorButton = withShortcut({
|
||||||
|
shortcut: "F7",
|
||||||
|
button: iconButton({
|
||||||
icon: squareFillIcon,
|
icon: squareFillIcon,
|
||||||
className: "forecolor",
|
className: "forecolor",
|
||||||
onClick: () => wrapWithForecolor(getForecolor()),
|
onClick: () => wrapWithForecolor(getForecolor()),
|
||||||
tooltip: tr.editingSetForegroundColourF7(),
|
tooltip: tr.editingSetForegroundColor(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const colorpickerButton = colorPicker({
|
const colorpickerButton = withShortcut({
|
||||||
|
shortcut: "F8",
|
||||||
|
button: colorPicker({
|
||||||
onChange: ({ currentTarget }) =>
|
onChange: ({ currentTarget }) =>
|
||||||
setForegroundColor((currentTarget as HTMLInputElement).value),
|
setForegroundColor((currentTarget as HTMLInputElement).value),
|
||||||
tooltip: tr.editingChangeColourF8(),
|
tooltip: tr.editingChangeColor(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return buttonGroup({
|
return buttonGroup({
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
commandIconButton,
|
commandIconButton,
|
||||||
iconButton,
|
iconButton,
|
||||||
buttonGroup,
|
buttonGroup,
|
||||||
|
withShortcut,
|
||||||
} from "editor-toolbar/dynamicComponents";
|
} from "editor-toolbar/dynamicComponents";
|
||||||
|
|
||||||
import boldIcon from "./type-bold.svg";
|
import boldIcon from "./type-bold.svg";
|
||||||
|
@ -20,42 +21,60 @@ import eraserIcon from "./eraser.svg";
|
||||||
|
|
||||||
export function getFormatInlineGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
export function getFormatInlineGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
ButtonGroupProps {
|
ButtonGroupProps {
|
||||||
const boldButton = commandIconButton({
|
const boldButton = withShortcut({
|
||||||
|
shortcut: "Control+KeyB",
|
||||||
|
button: commandIconButton({
|
||||||
icon: boldIcon,
|
icon: boldIcon,
|
||||||
tooltip: tr.editingBoldTextCtrlandb(),
|
tooltip: tr.editingBoldText(),
|
||||||
command: "bold",
|
command: "bold",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const italicButton = commandIconButton({
|
const italicButton = withShortcut({
|
||||||
|
shortcut: "Control+KeyI",
|
||||||
|
button: commandIconButton({
|
||||||
icon: italicIcon,
|
icon: italicIcon,
|
||||||
tooltip: tr.editingItalicTextCtrlandi(),
|
tooltip: tr.editingItalicText(),
|
||||||
command: "italic",
|
command: "italic",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const underlineButton = commandIconButton({
|
const underlineButton = withShortcut({
|
||||||
|
shortcut: "Control+KeyU",
|
||||||
|
button: commandIconButton({
|
||||||
icon: underlineIcon,
|
icon: underlineIcon,
|
||||||
tooltip: tr.editingUnderlineTextCtrlandu(),
|
tooltip: tr.editingUnderlineText(),
|
||||||
command: "underline",
|
command: "underline",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const superscriptButton = commandIconButton({
|
const superscriptButton = withShortcut({
|
||||||
|
shortcut: "Control+Shift+Equal",
|
||||||
|
button: commandIconButton({
|
||||||
icon: superscriptIcon,
|
icon: superscriptIcon,
|
||||||
tooltip: tr.editingSuperscriptCtrlandand(),
|
tooltip: tr.editingSuperscript(),
|
||||||
command: "superscript",
|
command: "superscript",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const subscriptButton = commandIconButton({
|
const subscriptButton = withShortcut({
|
||||||
|
shortcut: "Control+Equal",
|
||||||
|
button: commandIconButton({
|
||||||
icon: subscriptIcon,
|
icon: subscriptIcon,
|
||||||
tooltip: tr.editingSubscriptCtrland(),
|
tooltip: tr.editingSubscript(),
|
||||||
command: "subscript",
|
command: "subscript",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeFormatButton = iconButton({
|
const removeFormatButton = withShortcut({
|
||||||
|
shortcut: "Control+KeyR",
|
||||||
|
button: iconButton({
|
||||||
icon: eraserIcon,
|
icon: eraserIcon,
|
||||||
tooltip: tr.editingRemoveFormattingCtrlandr(),
|
tooltip: tr.editingRemoveFormatting(),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
document.execCommand("removeFormat");
|
document.execCommand("removeFormat");
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return buttonGroup({
|
return buttonGroup({
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { updateActiveButtons } from "editor-toolbar";
|
||||||
import { EditingArea } from "./editingArea";
|
import { EditingArea } from "./editingArea";
|
||||||
import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers";
|
import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers";
|
||||||
import { triggerChangeTimer } from "./changeTimer";
|
import { triggerChangeTimer } from "./changeTimer";
|
||||||
|
import { registerShortcut } from "anki/shortcuts";
|
||||||
|
|
||||||
export function onInput(event: Event): void {
|
export function onInput(event: Event): void {
|
||||||
// make sure IME changes get saved
|
// make sure IME changes get saved
|
||||||
|
@ -51,21 +52,19 @@ export function onKey(evt: KeyboardEvent): void {
|
||||||
triggerChangeTimer(currentField);
|
triggerChangeTimer(currentField);
|
||||||
}
|
}
|
||||||
|
|
||||||
globalThis.addEventListener("keydown", (evt: KeyboardEvent) => {
|
function updateFocus(evt: FocusEvent) {
|
||||||
if (evt.code === "Tab") {
|
|
||||||
globalThis.addEventListener(
|
|
||||||
"focusin",
|
|
||||||
(evt: FocusEvent) => {
|
|
||||||
const newFocusTarget = evt.target;
|
const newFocusTarget = evt.target;
|
||||||
if (newFocusTarget instanceof EditingArea) {
|
if (newFocusTarget instanceof EditingArea) {
|
||||||
caretToEnd(newFocusTarget);
|
caretToEnd(newFocusTarget);
|
||||||
updateActiveButtons();
|
updateActiveButtons();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
registerShortcut(
|
||||||
|
() => document.addEventListener("focusin", updateFocus, { once: true }),
|
||||||
|
"Tab",
|
||||||
|
["Shift"]
|
||||||
|
);
|
||||||
|
|
||||||
export function onKeyUp(evt: KeyboardEvent): void {
|
export function onKeyUp(evt: KeyboardEvent): void {
|
||||||
const currentField = evt.currentTarget as EditingArea;
|
const currentField = evt.currentTarget as EditingArea;
|
||||||
|
|
|
@ -6,7 +6,11 @@ import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||||
|
|
||||||
import { bridgeCommand } from "anki/bridgecommand";
|
import { bridgeCommand } from "anki/bridgecommand";
|
||||||
import * as tr from "anki/i18n";
|
import * as tr from "anki/i18n";
|
||||||
import { labelButton, buttonGroup } from "editor-toolbar/dynamicComponents";
|
import {
|
||||||
|
labelButton,
|
||||||
|
buttonGroup,
|
||||||
|
withShortcut,
|
||||||
|
} from "editor-toolbar/dynamicComponents";
|
||||||
|
|
||||||
export function getNotetypeGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
export function getNotetypeGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
ButtonGroupProps {
|
ButtonGroupProps {
|
||||||
|
@ -17,11 +21,14 @@ export function getNotetypeGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
tooltip: tr.editingCustomizeFields(),
|
tooltip: tr.editingCustomizeFields(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const cardsButton = labelButton({
|
const cardsButton = withShortcut({
|
||||||
|
shortcut: "Control+KeyL",
|
||||||
|
button: labelButton({
|
||||||
onClick: () => bridgeCommand("cards"),
|
onClick: () => bridgeCommand("cards"),
|
||||||
disables: false,
|
disables: false,
|
||||||
label: `${tr.editingCards()}...`,
|
label: `${tr.editingCards()}...`,
|
||||||
tooltip: tr.editingCustomizeCardTemplatesCtrlandl(),
|
tooltip: tr.editingCustomizeCardTemplates(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return buttonGroup({
|
return buttonGroup({
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
dropdownMenu,
|
dropdownMenu,
|
||||||
dropdownItem,
|
dropdownItem,
|
||||||
buttonGroup,
|
buttonGroup,
|
||||||
|
withShortcut,
|
||||||
} from "editor-toolbar/dynamicComponents";
|
} from "editor-toolbar/dynamicComponents";
|
||||||
import * as tr from "anki/i18n";
|
import * as tr from "anki/i18n";
|
||||||
|
|
||||||
|
@ -41,21 +42,26 @@ const mathjaxMenuId = "mathjaxMenu";
|
||||||
|
|
||||||
export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
ButtonGroupProps {
|
ButtonGroupProps {
|
||||||
const attachmentButton = iconButton({
|
const attachmentButton = withShortcut({
|
||||||
|
shortcut: "F3",
|
||||||
|
button: iconButton({
|
||||||
icon: paperclipIcon,
|
icon: paperclipIcon,
|
||||||
onClick: onAttachment,
|
onClick: onAttachment,
|
||||||
tooltip: tr.editingAttachPicturesaudiovideoF3(),
|
tooltip: tr.editingAttachPicturesaudiovideo(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const recordButton = iconButton({
|
const recordButton = withShortcut({
|
||||||
|
shortcut: "F5",
|
||||||
|
button: iconButton({
|
||||||
icon: micIcon,
|
icon: micIcon,
|
||||||
onClick: onRecord,
|
onClick: onRecord,
|
||||||
tooltip: tr.editingRecordAudioF5(),
|
tooltip: tr.editingRecordAudio(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mathjaxButton = iconButton({
|
const mathjaxButton = iconButton({
|
||||||
icon: functionIcon,
|
icon: functionIcon,
|
||||||
foo: 5,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mathjaxButtonWithMenu = withDropdownMenu({
|
const mathjaxButtonWithMenu = withDropdownMenu({
|
||||||
|
@ -63,10 +69,13 @@ export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
menuId: mathjaxMenuId,
|
menuId: mathjaxMenuId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const htmlButton = iconButton({
|
const htmlButton = withShortcut({
|
||||||
|
shortcut: "Control+Shift+KeyX",
|
||||||
|
button: iconButton({
|
||||||
icon: xmlIcon,
|
icon: xmlIcon,
|
||||||
onClick: onHtmlEdit,
|
onClick: onHtmlEdit,
|
||||||
tooltip: tr.editingHtmlEditor,
|
tooltip: tr.editingHtmlEditor(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return buttonGroup({
|
return buttonGroup({
|
||||||
|
@ -84,38 +93,50 @@ export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
export function getTemplateMenus(): (DynamicSvelteComponent<typeof DropdownMenu> &
|
export function getTemplateMenus(): (DynamicSvelteComponent<typeof DropdownMenu> &
|
||||||
DropdownMenuProps)[] {
|
DropdownMenuProps)[] {
|
||||||
const mathjaxMenuItems = [
|
const mathjaxMenuItems = [
|
||||||
dropdownItem({
|
withShortcut({
|
||||||
|
shortcut: "Control+KeyM, KeyM",
|
||||||
|
button: dropdownItem({
|
||||||
onClick: () => wrap("\\(", "\\)"),
|
onClick: () => wrap("\\(", "\\)"),
|
||||||
label: tr.editingMathjaxInline(),
|
label: tr.editingMathjaxInline(),
|
||||||
endLabel: "Ctrl+M, M",
|
|
||||||
}),
|
}),
|
||||||
dropdownItem({
|
}),
|
||||||
|
withShortcut({
|
||||||
|
shortcut: "Control+KeyM, KeyE",
|
||||||
|
button: dropdownItem({
|
||||||
onClick: () => wrap("\\[", "\\]"),
|
onClick: () => wrap("\\[", "\\]"),
|
||||||
label: tr.editingMathjaxBlock(),
|
label: tr.editingMathjaxBlock(),
|
||||||
endLabel: "Ctrl+M, E",
|
|
||||||
}),
|
}),
|
||||||
dropdownItem({
|
}),
|
||||||
|
withShortcut({
|
||||||
|
shortcut: "Control+KeyM, KeyC",
|
||||||
|
button: dropdownItem({
|
||||||
onClick: () => wrap("\\(\\ce{", "}\\)"),
|
onClick: () => wrap("\\(\\ce{", "}\\)"),
|
||||||
label: tr.editingMathjaxChemistry(),
|
label: tr.editingMathjaxChemistry(),
|
||||||
endLabel: "Ctrl+M, C",
|
}),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const latexMenuItems = [
|
const latexMenuItems = [
|
||||||
dropdownItem({
|
withShortcut({
|
||||||
|
shortcut: "Control+KeyT, KeyT",
|
||||||
|
button: dropdownItem({
|
||||||
onClick: () => wrap("[latex]", "[/latex]"),
|
onClick: () => wrap("[latex]", "[/latex]"),
|
||||||
label: tr.editingLatex(),
|
label: tr.editingLatex(),
|
||||||
endLabel: "Ctrl+T, T",
|
|
||||||
}),
|
}),
|
||||||
dropdownItem({
|
}),
|
||||||
|
withShortcut({
|
||||||
|
shortcut: "Control+KeyT, KeyE",
|
||||||
|
button: dropdownItem({
|
||||||
onClick: () => wrap("[$]", "[/$]"),
|
onClick: () => wrap("[$]", "[/$]"),
|
||||||
label: tr.editingLatexEquation(),
|
label: tr.editingLatexEquation(),
|
||||||
endLabel: "Ctrl+T, E",
|
|
||||||
}),
|
}),
|
||||||
dropdownItem({
|
}),
|
||||||
|
withShortcut({
|
||||||
|
shortcut: "Control+KeyT, KeyM",
|
||||||
|
button: dropdownItem({
|
||||||
onClick: () => wrap("[$$]", "[/$$]"),
|
onClick: () => wrap("[$$]", "[/$$]"),
|
||||||
label: tr.editingLatexMathEnv(),
|
label: tr.editingLatexMathEnv(),
|
||||||
endLabel: "Ctrl+T, M",
|
}),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
149
ts/lib/shortcuts.ts
Normal file
149
ts/lib/shortcuts.ts
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import * as tr from "./i18n";
|
||||||
|
|
||||||
|
export type Modifier = "Control" | "Alt" | "Shift" | "Meta";
|
||||||
|
|
||||||
|
const modifiers: Modifier[] = ["Control", "Alt", "Shift", "Meta"];
|
||||||
|
|
||||||
|
// how modifiers are mapped
|
||||||
|
const platformModifiers =
|
||||||
|
navigator.platform === "MacIntel"
|
||||||
|
? ["Meta", "Alt", "Shift", "Control"]
|
||||||
|
: ["Control", "Alt", "Shift", "OS"];
|
||||||
|
|
||||||
|
function splitKeyCombinationString(keyCombinationString: string): string[][] {
|
||||||
|
return keyCombinationString.split(", ").map((segment) => segment.split("+"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function modifiersToPlatformString(modifiers: string[]): string {
|
||||||
|
const displayModifiers =
|
||||||
|
navigator.platform === "MacIntel"
|
||||||
|
? ["^", "⌥", "⇧", "⌘"]
|
||||||
|
: [`${tr.keyboardCtrl()}+`, "Alt+", `${tr.keyboardShift()}+`, "Win+"];
|
||||||
|
|
||||||
|
let result = "";
|
||||||
|
|
||||||
|
for (const modifier of modifiers) {
|
||||||
|
result += displayModifiers[platformModifiers.indexOf(modifier)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alphabeticPrefix = "Key";
|
||||||
|
const numericPrefix = "Digit";
|
||||||
|
const keyToCharacterMap = {
|
||||||
|
Backslash: "\\",
|
||||||
|
Backquote: "`",
|
||||||
|
BracketLeft: "[",
|
||||||
|
BrackerRight: "]",
|
||||||
|
Quote: "'",
|
||||||
|
Semicolon: ";",
|
||||||
|
Minus: "-",
|
||||||
|
Equal: "=",
|
||||||
|
Comma: ",",
|
||||||
|
Period: ".",
|
||||||
|
Slash: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
function keyToPlatformString(key: string): string {
|
||||||
|
return key.startsWith(alphabeticPrefix)
|
||||||
|
? key.slice(alphabeticPrefix.length)
|
||||||
|
: key.startsWith(numericPrefix)
|
||||||
|
? key.slice(numericPrefix.length)
|
||||||
|
: Object.prototype.hasOwnProperty.call(keyToCharacterMap, key)
|
||||||
|
? keyToCharacterMap[key]
|
||||||
|
: key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPlatformString(modifiersAndKey: string[]): string {
|
||||||
|
return `${modifiersToPlatformString(
|
||||||
|
modifiersAndKey.slice(0, -1)
|
||||||
|
)}${keyToPlatformString(modifiersAndKey[modifiersAndKey.length - 1])}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlatformString(keyCombinationString: string): string {
|
||||||
|
return splitKeyCombinationString(keyCombinationString)
|
||||||
|
.map(toPlatformString)
|
||||||
|
.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkKey(event: KeyboardEvent, key: string): boolean {
|
||||||
|
return event.code === key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkModifiers(
|
||||||
|
event: KeyboardEvent,
|
||||||
|
optionalModifiers: Modifier[],
|
||||||
|
activeModifiers: string[]
|
||||||
|
): boolean {
|
||||||
|
return modifiers.reduce(
|
||||||
|
(matches: boolean, modifier: string, currentIndex: number): boolean =>
|
||||||
|
matches &&
|
||||||
|
(optionalModifiers.includes(modifier as Modifier) ||
|
||||||
|
event.getModifierState(platformModifiers[currentIndex]) ===
|
||||||
|
activeModifiers.includes(modifier)),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function check(
|
||||||
|
event: KeyboardEvent,
|
||||||
|
optionalModifiers: Modifier[],
|
||||||
|
modifiersAndKey: string[]
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
checkKey(event, modifiersAndKey[modifiersAndKey.length - 1]) &&
|
||||||
|
checkModifiers(event, optionalModifiers, modifiersAndKey.slice(0, -1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcutTimeoutMs = 400;
|
||||||
|
|
||||||
|
function innerShortcut(
|
||||||
|
lastEvent: KeyboardEvent,
|
||||||
|
callback: (event: KeyboardEvent) => void,
|
||||||
|
optionalModifiers: Modifier[],
|
||||||
|
...keyCombination: string[][]
|
||||||
|
): void {
|
||||||
|
let interval: number;
|
||||||
|
|
||||||
|
if (keyCombination.length === 0) {
|
||||||
|
callback(lastEvent);
|
||||||
|
} else {
|
||||||
|
const [nextKey, ...restKeys] = keyCombination;
|
||||||
|
|
||||||
|
const handler = (event: KeyboardEvent): void => {
|
||||||
|
if (check(event, optionalModifiers, nextKey)) {
|
||||||
|
innerShortcut(event, callback, optionalModifiers, ...restKeys);
|
||||||
|
clearTimeout(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interval = setTimeout(
|
||||||
|
(): void => document.removeEventListener("keydown", handler),
|
||||||
|
shortcutTimeoutMs
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handler, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerShortcut(
|
||||||
|
callback: (event: KeyboardEvent) => void,
|
||||||
|
keyCombinationString: string,
|
||||||
|
optionalModifiers: Modifier[] = []
|
||||||
|
): () => void {
|
||||||
|
const keyCombination = splitKeyCombinationString(keyCombinationString);
|
||||||
|
const [firstKey, ...restKeys] = keyCombination;
|
||||||
|
|
||||||
|
const handler = (event: KeyboardEvent): void => {
|
||||||
|
if (check(event, optionalModifiers, firstKey)) {
|
||||||
|
innerShortcut(event, callback, optionalModifiers, ...restKeys);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handler);
|
||||||
|
return (): void => document.removeEventListener("keydown", handler);
|
||||||
|
}
|
Loading…
Reference in a new issue