Merge pull request #1146 from hgiesel/shortcuts
Shortcut API for Editor
This commit is contained in:
commit
02ebab7491
@ -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
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:
|
||||
# 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)
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
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 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
|
||||
);
|
||||
|
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
|
||||
// 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(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -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
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 New Issue
Block a user