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-right = Align right
editing-an-error-occurred-while-opening = An error occurred while opening { $val }
editing-attach-picturesaudiovideo-f3 = Attach pictures/audio/video (F3)
editing-bold-text-ctrlandb = Bold text (Ctrl+B)
editing-attach-picturesaudiovideo = Attach pictures/audio/video
editing-bold-text = Bold text
editing-cards = Cards
editing-center = Center
editing-change-colour-f8 = Change colour (F8)
editing-cloze-deletion-ctrlandshiftandc = Cloze deletion (Ctrl+Shift+C)
editing-change-color = Change color
editing-cloze-deletion = Cloze deletion
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-cut = Cut
editing-edit-current = Edit Current
@ -17,7 +17,7 @@ editing-edit-html = Edit HTML
editing-fields = Fields
editing-html-editor = HTML Editor
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-justify = Justify
editing-latex = LaTeX
@ -30,14 +30,29 @@ editing-media = Media
editing-ordered-list = Ordered list
editing-outdent = Decrease indent
editing-paste = Paste
editing-record-audio-f5 = Record audio (F5)
editing-remove-formatting-ctrlandr = Remove formatting (Ctrl+R)
editing-set-foreground-colour-f7 = Set foreground colour (F7)
editing-record-audio = Record audio
editing-remove-formatting = Remove formatting
editing-set-foreground-color = Set foreground color
editing-show-duplicates = Show Duplicates
editing-subscript-ctrland = Subscript (Ctrl+=)
editing-superscript-ctrlandand = Superscript (Ctrl++)
editing-subscript = Subscript
editing-superscript = Superscript
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-underline-text-ctrlandu = Underline text (Ctrl+U)
editing-underline-text = Underline text
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.
## 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:
# if a third element is provided, enable shortcut even when no field selected
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),
]
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
-->
<script lang="typescript">
import { getContext } from "svelte";
import { onMount, createEventDispatcher, getContext } from "svelte";
import { nightModeKey } from "./contextKeys";
import { mergeTooltipAndShortcut } from "./helpers";
export let id: string;
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;
@ -18,11 +22,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const nightMode = getContext(nightModeKey);
let buttonRef: HTMLButtonElement;
let inputRef: HTMLInputElement;
function delegateToInput() {
inputRef.click();
}
const dispatch = createEventDispatcher();
onMount(() => dispatch("mount", { button: buttonRef }));
</script>
<style lang="scss">
@ -57,13 +65,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</style>
<button
bind:this={buttonRef}
tabindex="-1"
{id}
class={extendClassName(className)}
class:btn-day={!nightMode}
class:btn-night={nightMode}
title={tooltip}
{title}
on:click={delegateToInput}
on:mousedown|preventDefault>
<input bind:this={inputRef} type="color" on:change={onChange} />
<input tabindex="-1" bind:this={inputRef} type="color" on:change={onChange} />
</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 className = "";
export let tooltip: string;
export let shortcutLabel: string | undefined;
export let icon: string;
export let command: string;
@ -80,6 +81,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{id}
{className}
{tooltip}
{shortcutLabel}
{active}
{disables}
{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
-->
<script lang="typescript">
import { getContext } from "svelte";
import { onMount, createEventDispatcher, getContext } from "svelte";
import { nightModeKey } from "./contextKeys";
export let id: string;
export let className = "";
export let tooltip: string;
export let label: string;
export let shortcutLabel: string | undefined;
export let onClick: (event: MouseEvent) => void;
export let label: string;
export let endLabel: string;
let buttonRef: HTMLButtonElement;
const nightMode = getContext(nightModeKey);
const dispatch = createEventDispatcher();
onMount(() => dispatch("mount", { button: buttonRef }));
</script>
<style lang="scss">
@ -60,12 +65,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<button
{id}
bind:this={buttonRef}
class={`btn dropdown-item ${className}`}
class:btn-day={!nightMode}
class:btn-night={nightMode}
title={tooltip}
on:click={onClick}
on:mousedown|preventDefault>
<span class:me-3={endLabel}>{label}</span>
{#if endLabel}<span class="monospace">{endLabel}</span>{/if}
<span class:me-3={shortcutLabel}>{label}</span>
{#if shortcutLabel}<span class="monospace">{shortcutLabel}</span>{/if}
</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 className = "";
export let tooltip: string;
export let tooltip: string | undefined;
export let shortcutLabel: string | undefined;
export let icon: string;
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;
</script>
<SquareButton {id} {className} {tooltip} {onClick} {disables} {dropdownToggle} on:mount>
<SquareButton
{id}
{className}
{tooltip}
{shortcutLabel}
{onClick}
{disables}
{dropdownToggle}
on:mount>
{@html icon}
</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 { onMount, createEventDispatcher, getContext } from "svelte";
import { disabledKey, nightModeKey } from "./contextKeys";
import { mergeTooltipAndShortcut } from "./helpers";
export let id: string;
export let className = "";
export let tooltip: string | undefined;
export let shortcutLabel: string | undefined;
export let label: string;
export let tooltip: string;
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
export let onClick: (event: MouseEvent) => void;
export let disables = true;
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}
tabindex="-1"
disabled={_disabled}
title={tooltip}
{title}
{...extraProps}
on:click={onClick}
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 { getContext, onMount, createEventDispatcher } from "svelte";
import { disabledKey, nightModeKey } from "./contextKeys";
import { mergeTooltipAndShortcut } from "./helpers";
export let id: string;
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 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-night={nightMode}
tabindex="-1"
title={tooltip}
{title}
disabled={_disabled}
{...extraProps}
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 type { WithDropdownMenuProps } from "./WithDropdownMenu";
import WithShortcut from "./WithShortcut.svelte";
import type { WithShortcutProps } from "./WithShortcut";
import { dynamicComponent } from "sveltelib/dynamicComponent";
export const labelButton = dynamicComponent<typeof LabelButton, LabelButtonProps>(
@ -55,3 +58,7 @@ export const withDropdownMenu = dynamicComponent<
typeof WithDropdownMenu,
WithDropdownMenuProps
>(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
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type IconButton from "editor-toolbar/IconButton.svelte";
import type { IconButtonProps } from "editor-toolbar/IconButton";
import type WithShortcut from "editor-toolbar/WithShortcut.svelte";
import type { WithShortcutProps } from "editor-toolbar/WithShortcut";
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
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";
@ -35,17 +35,21 @@ function getCurrentHighestCloze(increment: boolean): number {
return Math.max(1, highest);
}
function onCloze(event: MouseEvent): void {
const highestCloze = getCurrentHighestCloze(!event.altKey);
function onCloze(event: KeyboardEvent | MouseEvent): void {
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
wrap(`{{c${highestCloze}::`, "}}");
}
export function getClozeButton(): DynamicSvelteComponent<typeof IconButton> &
IconButtonProps {
return iconButton({
export function getClozeButton(): DynamicSvelteComponent<typeof WithShortcut> &
WithShortcutProps {
return withShortcut({
id: "cloze",
icon: bracketsIcon,
onClick: onCloze,
tooltip: tr.editingClozeDeletionCtrlandshiftandc(),
shortcut: "Control+Shift+KeyC",
optionalModifiers: ["Alt"],
button: iconButton({
icon: bracketsIcon,
onClick: onCloze,
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 { 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 squareFillIcon from "./square-fill.svg";
@ -26,17 +31,23 @@ function wrapWithForecolor(color: string): void {
export function getColorGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
ButtonGroupProps {
const forecolorButton = iconButton({
icon: squareFillIcon,
className: "forecolor",
onClick: () => wrapWithForecolor(getForecolor()),
tooltip: tr.editingSetForegroundColourF7(),
const forecolorButton = withShortcut({
shortcut: "F7",
button: iconButton({
icon: squareFillIcon,
className: "forecolor",
onClick: () => wrapWithForecolor(getForecolor()),
tooltip: tr.editingSetForegroundColor(),
}),
});
const colorpickerButton = colorPicker({
onChange: ({ currentTarget }) =>
setForegroundColor((currentTarget as HTMLInputElement).value),
tooltip: tr.editingChangeColourF8(),
const colorpickerButton = withShortcut({
shortcut: "F8",
button: colorPicker({
onChange: ({ currentTarget }) =>
setForegroundColor((currentTarget as HTMLInputElement).value),
tooltip: tr.editingChangeColor(),
}),
});
return buttonGroup({

View File

@ -9,6 +9,7 @@ import {
commandIconButton,
iconButton,
buttonGroup,
withShortcut,
} from "editor-toolbar/dynamicComponents";
import boldIcon from "./type-bold.svg";
@ -20,42 +21,60 @@ import eraserIcon from "./eraser.svg";
export function getFormatInlineGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
ButtonGroupProps {
const boldButton = commandIconButton({
icon: boldIcon,
tooltip: tr.editingBoldTextCtrlandb(),
command: "bold",
const boldButton = withShortcut({
shortcut: "Control+KeyB",
button: commandIconButton({
icon: boldIcon,
tooltip: tr.editingBoldText(),
command: "bold",
}),
});
const italicButton = commandIconButton({
icon: italicIcon,
tooltip: tr.editingItalicTextCtrlandi(),
command: "italic",
const italicButton = withShortcut({
shortcut: "Control+KeyI",
button: commandIconButton({
icon: italicIcon,
tooltip: tr.editingItalicText(),
command: "italic",
}),
});
const underlineButton = commandIconButton({
icon: underlineIcon,
tooltip: tr.editingUnderlineTextCtrlandu(),
command: "underline",
const underlineButton = withShortcut({
shortcut: "Control+KeyU",
button: commandIconButton({
icon: underlineIcon,
tooltip: tr.editingUnderlineText(),
command: "underline",
}),
});
const superscriptButton = commandIconButton({
icon: superscriptIcon,
tooltip: tr.editingSuperscriptCtrlandand(),
command: "superscript",
const superscriptButton = withShortcut({
shortcut: "Control+Shift+Equal",
button: commandIconButton({
icon: superscriptIcon,
tooltip: tr.editingSuperscript(),
command: "superscript",
}),
});
const subscriptButton = commandIconButton({
icon: subscriptIcon,
tooltip: tr.editingSubscriptCtrland(),
command: "subscript",
const subscriptButton = withShortcut({
shortcut: "Control+Equal",
button: commandIconButton({
icon: subscriptIcon,
tooltip: tr.editingSubscript(),
command: "subscript",
}),
});
const removeFormatButton = iconButton({
icon: eraserIcon,
tooltip: tr.editingRemoveFormattingCtrlandr(),
onClick: () => {
document.execCommand("removeFormat");
},
const removeFormatButton = withShortcut({
shortcut: "Control+KeyR",
button: iconButton({
icon: eraserIcon,
tooltip: tr.editingRemoveFormatting(),
onClick: () => {
document.execCommand("removeFormat");
},
}),
});
return buttonGroup({

View File

@ -5,6 +5,7 @@ import { updateActiveButtons } from "editor-toolbar";
import { EditingArea } from "./editingArea";
import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers";
import { triggerChangeTimer } from "./changeTimer";
import { registerShortcut } from "anki/shortcuts";
export function onInput(event: Event): void {
// make sure IME changes get saved
@ -51,21 +52,19 @@ export function onKey(evt: KeyboardEvent): void {
triggerChangeTimer(currentField);
}
globalThis.addEventListener("keydown", (evt: KeyboardEvent) => {
if (evt.code === "Tab") {
globalThis.addEventListener(
"focusin",
(evt: FocusEvent) => {
const newFocusTarget = evt.target;
if (newFocusTarget instanceof EditingArea) {
caretToEnd(newFocusTarget);
updateActiveButtons();
}
},
{ once: true }
);
function updateFocus(evt: FocusEvent) {
const newFocusTarget = evt.target;
if (newFocusTarget instanceof EditingArea) {
caretToEnd(newFocusTarget);
updateActiveButtons();
}
});
}
registerShortcut(
() => document.addEventListener("focusin", updateFocus, { once: true }),
"Tab",
["Shift"]
);
export function onKeyUp(evt: KeyboardEvent): void {
const currentField = evt.currentTarget as EditingArea;

View File

@ -6,7 +6,11 @@ import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import { bridgeCommand } from "anki/bridgecommand";
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> &
ButtonGroupProps {
@ -17,11 +21,14 @@ export function getNotetypeGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
tooltip: tr.editingCustomizeFields(),
});
const cardsButton = labelButton({
onClick: () => bridgeCommand("cards"),
disables: false,
label: `${tr.editingCards()}...`,
tooltip: tr.editingCustomizeCardTemplatesCtrlandl(),
const cardsButton = withShortcut({
shortcut: "Control+KeyL",
button: labelButton({
onClick: () => bridgeCommand("cards"),
disables: false,
label: `${tr.editingCards()}...`,
tooltip: tr.editingCustomizeCardTemplates(),
}),
});
return buttonGroup({

View File

@ -13,6 +13,7 @@ import {
dropdownMenu,
dropdownItem,
buttonGroup,
withShortcut,
} from "editor-toolbar/dynamicComponents";
import * as tr from "anki/i18n";
@ -41,21 +42,26 @@ const mathjaxMenuId = "mathjaxMenu";
export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
ButtonGroupProps {
const attachmentButton = iconButton({
icon: paperclipIcon,
onClick: onAttachment,
tooltip: tr.editingAttachPicturesaudiovideoF3(),
const attachmentButton = withShortcut({
shortcut: "F3",
button: iconButton({
icon: paperclipIcon,
onClick: onAttachment,
tooltip: tr.editingAttachPicturesaudiovideo(),
}),
});
const recordButton = iconButton({
icon: micIcon,
onClick: onRecord,
tooltip: tr.editingRecordAudioF5(),
const recordButton = withShortcut({
shortcut: "F5",
button: iconButton({
icon: micIcon,
onClick: onRecord,
tooltip: tr.editingRecordAudio(),
}),
});
const mathjaxButton = iconButton({
icon: functionIcon,
foo: 5,
});
const mathjaxButtonWithMenu = withDropdownMenu({
@ -63,10 +69,13 @@ export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
menuId: mathjaxMenuId,
});
const htmlButton = iconButton({
icon: xmlIcon,
onClick: onHtmlEdit,
tooltip: tr.editingHtmlEditor,
const htmlButton = withShortcut({
shortcut: "Control+Shift+KeyX",
button: iconButton({
icon: xmlIcon,
onClick: onHtmlEdit,
tooltip: tr.editingHtmlEditor(),
}),
});
return buttonGroup({
@ -84,38 +93,50 @@ export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
export function getTemplateMenus(): (DynamicSvelteComponent<typeof DropdownMenu> &
DropdownMenuProps)[] {
const mathjaxMenuItems = [
dropdownItem({
onClick: () => wrap("\\(", "\\)"),
label: tr.editingMathjaxInline(),
endLabel: "Ctrl+M, M",
withShortcut({
shortcut: "Control+KeyM, KeyM",
button: dropdownItem({
onClick: () => wrap("\\(", "\\)"),
label: tr.editingMathjaxInline(),
}),
}),
dropdownItem({
onClick: () => wrap("\\[", "\\]"),
label: tr.editingMathjaxBlock(),
endLabel: "Ctrl+M, E",
withShortcut({
shortcut: "Control+KeyM, KeyE",
button: dropdownItem({
onClick: () => wrap("\\[", "\\]"),
label: tr.editingMathjaxBlock(),
}),
}),
dropdownItem({
onClick: () => wrap("\\(\\ce{", "}\\)"),
label: tr.editingMathjaxChemistry(),
endLabel: "Ctrl+M, C",
withShortcut({
shortcut: "Control+KeyM, KeyC",
button: dropdownItem({
onClick: () => wrap("\\(\\ce{", "}\\)"),
label: tr.editingMathjaxChemistry(),
}),
}),
];
const latexMenuItems = [
dropdownItem({
onClick: () => wrap("[latex]", "[/latex]"),
label: tr.editingLatex(),
endLabel: "Ctrl+T, T",
withShortcut({
shortcut: "Control+KeyT, KeyT",
button: dropdownItem({
onClick: () => wrap("[latex]", "[/latex]"),
label: tr.editingLatex(),
}),
}),
dropdownItem({
onClick: () => wrap("[$]", "[/$]"),
label: tr.editingLatexEquation(),
endLabel: "Ctrl+T, E",
withShortcut({
shortcut: "Control+KeyT, KeyE",
button: dropdownItem({
onClick: () => wrap("[$]", "[/$]"),
label: tr.editingLatexEquation(),
}),
}),
dropdownItem({
onClick: () => wrap("[$$]", "[/$$]"),
label: tr.editingLatexMathEnv(),
endLabel: "Ctrl+T, M",
withShortcut({
shortcut: "Control+KeyT, KeyM",
button: dropdownItem({
onClick: () => wrap("[$$]", "[/$$]"),
label: tr.editingLatexMathEnv(),
}),
}),
];

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