Merge pull request #1146 from hgiesel/shortcuts

Shortcut API for Editor
This commit is contained in:
Damien Elmes 2021-04-23 09:44:01 +10:00 committed by GitHub
commit 02ebab7491
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 469 additions and 152 deletions

View file

@ -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
View file

@ -0,0 +1,2 @@
keyboard-ctrl = Ctrl
keyboard-shift = Shift

View file

@ -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)

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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
View 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[];
}

View 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} />

View file

@ -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
);

View 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;
}

View file

@ -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(),
}),
}); });
} }

View file

@ -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({

View file

@ -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({

View file

@ -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;

View file

@ -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({

View file

@ -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
View 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);
}