HotKeys Bug Fixes
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"@tauri-apps/plugin-opener": "^2.5.4",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-typst": "^0.4.0",
|
||||
"hotkeys-js": "^4.0.4",
|
||||
"y-codemirror.next": "^0.3.5",
|
||||
"y-websocket": "^3.0.0",
|
||||
"yjs": "^13.6.31",
|
||||
@@ -345,6 +346,8 @@
|
||||
|
||||
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"hotkeys-js": ["[email protected]", "", {}, "sha512-hseNiqaskxSnujuGp8aRMLJfcjaFiTSS0I2GQhqru82N/sx6CGyUf6pvU5X1iycvw2EqmvILkFIb5OzYFXY+9A=="],
|
||||
|
||||
"is-reference": ["[email protected]", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"isomorphic.js": ["[email protected]", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"@tauri-apps/plugin-opener": "^2.5.4",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-typst": "^0.4.0",
|
||||
"hotkeys-js": "^4.0.4",
|
||||
"y-codemirror.next": "^0.3.5",
|
||||
"y-websocket": "^3.0.0",
|
||||
"yjs": "^13.6.31"
|
||||
|
||||
@@ -7,12 +7,7 @@
|
||||
highlightActiveLine,
|
||||
} from "@codemirror/view";
|
||||
import { EditorState, Compartment, StateField } from "@codemirror/state";
|
||||
import {
|
||||
defaultKeymap,
|
||||
history,
|
||||
historyKeymap,
|
||||
indentWithTab,
|
||||
} from "@codemirror/commands";
|
||||
import { defaultKeymap, history, indentWithTab } from "@codemirror/commands";
|
||||
import {
|
||||
bracketMatching,
|
||||
indentOnInput,
|
||||
@@ -46,7 +41,6 @@
|
||||
diagnostics?: Diagnostic[];
|
||||
collab?: { text: Y.Text; awareness: Awareness } | null;
|
||||
onchange: (value: string) => void;
|
||||
onsave: () => void;
|
||||
onlspstatus?: (status: "off" | "starting" | "on" | "unavailable") => void;
|
||||
onready?: (view: EditorView | null) => void;
|
||||
}
|
||||
@@ -59,7 +53,6 @@
|
||||
diagnostics = [],
|
||||
collab = null,
|
||||
onchange,
|
||||
onsave,
|
||||
onlspstatus,
|
||||
onready,
|
||||
}: Props = $props();
|
||||
@@ -170,17 +163,8 @@
|
||||
: [autocompletion({ override: [typstCompletions] })]),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
{
|
||||
key: "Mod-s",
|
||||
preventDefault: true,
|
||||
run: () => {
|
||||
onsave();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
indentWithTab,
|
||||
]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
import Modal from "./Modal.svelte";
|
||||
import * as api from "$lib/ts/api";
|
||||
import type { AppInfo, CompatibilityStatus } from "$lib/ts/api";
|
||||
import {
|
||||
HOTKEY_DEFS,
|
||||
comboFromEvent,
|
||||
isCustomized,
|
||||
keysFor,
|
||||
rebindHotkey,
|
||||
resetHotkey,
|
||||
} from "$lib/ts/hotkeys";
|
||||
import {
|
||||
app,
|
||||
applyTheme,
|
||||
@@ -35,6 +43,7 @@
|
||||
| "accessibility"
|
||||
| "account"
|
||||
| "lsp"
|
||||
| "hotkeys"
|
||||
| "about";
|
||||
|
||||
const sections: { id: Section; label: string; icon: string }[] = [
|
||||
@@ -43,9 +52,115 @@
|
||||
{ id: "accessibility", label: "Accessibility", icon: "ph:wheelchair" },
|
||||
{ id: "account", label: "Account", icon: "ph:user-circle" },
|
||||
{ id: "lsp", label: "Language Server", icon: "ph:plugs-connected" },
|
||||
{ id: "hotkeys", label: "Hotkeys", icon: "ph:keyboard" },
|
||||
{ id: "about", label: "About", icon: "ph:info" },
|
||||
];
|
||||
|
||||
const isMac =
|
||||
typeof navigator !== "undefined" &&
|
||||
/mac/i.test(navigator.platform ?? navigator.userAgent);
|
||||
|
||||
function formatKeys(combo: string): string[] {
|
||||
const variants = combo.split(",");
|
||||
const preferred =
|
||||
variants.find((variant) =>
|
||||
isMac ? variant.includes("command") : !variant.includes("command"),
|
||||
) ?? variants[0];
|
||||
|
||||
return preferred.split("+").map((part) => {
|
||||
switch (part) {
|
||||
case "command":
|
||||
return "⌘";
|
||||
case "ctrl":
|
||||
return "Ctrl";
|
||||
case "alt":
|
||||
return isMac ? "⌥" : "Alt";
|
||||
case "shift":
|
||||
return "Shift";
|
||||
case "esc":
|
||||
return "Esc";
|
||||
case "space":
|
||||
return "Space";
|
||||
case "up":
|
||||
return "↑";
|
||||
case "down":
|
||||
return "↓";
|
||||
case "left":
|
||||
return "←";
|
||||
case "right":
|
||||
return "→";
|
||||
default:
|
||||
return part.length === 1 ? part.toUpperCase() : part;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let editingId = $state<string | null>(null);
|
||||
let hotkeyVersion = $state(0);
|
||||
|
||||
const editableHotkeyGroups = $derived.by(() => {
|
||||
hotkeyVersion;
|
||||
const groups = new Map<string, typeof HOTKEY_DEFS>();
|
||||
for (const def of HOTKEY_DEFS) {
|
||||
if (!groups.has(def.group)) groups.set(def.group, []);
|
||||
groups.get(def.group)!.push(def);
|
||||
}
|
||||
return Array.from(groups.entries()).map(([title, defs]) => ({
|
||||
title,
|
||||
items: defs.map((def) => ({
|
||||
id: def.id,
|
||||
label: def.label,
|
||||
keys: keysFor(def.id),
|
||||
customized: isCustomized(def.id),
|
||||
})),
|
||||
}));
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!editingId) return;
|
||||
const id = editingId;
|
||||
|
||||
function handleCapture(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.key === "Escape") {
|
||||
editingId = null;
|
||||
return;
|
||||
}
|
||||
const combo = comboFromEvent(event);
|
||||
if (!combo) return;
|
||||
rebindHotkey(id, combo);
|
||||
hotkeyVersion++;
|
||||
editingId = null;
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleCapture, true);
|
||||
return () => window.removeEventListener("keydown", handleCapture, true);
|
||||
});
|
||||
|
||||
const staticHotkeyGroups: { title: string; items: { keys: string[]; label: string }[] }[] = [
|
||||
{
|
||||
title: "File browser",
|
||||
items: [
|
||||
{ keys: ["F2"], label: "Rename selected entry" },
|
||||
{ keys: ["Delete"], label: "Delete selected entries" },
|
||||
{ keys: ["Esc"], label: "Cancel rename or clear selection" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Image viewer",
|
||||
items: [
|
||||
{ keys: ["←"], label: "Previous image" },
|
||||
{ keys: ["→"], label: "Next image" },
|
||||
{ keys: ["Esc"], label: "Close viewer" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "General",
|
||||
items: [{ keys: ["Esc"], label: "Close dialog" }],
|
||||
},
|
||||
];
|
||||
|
||||
const lspLabel: Record<string, string> = {
|
||||
off: "Off",
|
||||
starting: "Starting…",
|
||||
@@ -507,6 +622,88 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if section === "hotkeys"}
|
||||
<div class="flex flex-col gap-5">
|
||||
<p class="text-xs text-[var(--color-ink-muted)]">
|
||||
Click the pencil next to a shortcut and press a new key combination.
|
||||
Press Esc while listening to cancel.
|
||||
</p>
|
||||
|
||||
{#each editableHotkeyGroups as group}
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-[var(--color-ink-muted)]">
|
||||
{group.title}
|
||||
</span>
|
||||
<div class="flex flex-col divide-y divide-[var(--color-line)] rounded-md border border-[var(--color-line)]">
|
||||
{#each group.items as item}
|
||||
<div class="flex items-center justify-between gap-4 px-3 py-2">
|
||||
<span class="text-xs">{item.label}</span>
|
||||
{#if editingId === item.id}
|
||||
<span class="text-xs text-[var(--color-accent)]">
|
||||
Press keys… (Esc to cancel)
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex shrink-0 items-center gap-1">
|
||||
{#each formatKeys(item.keys) as key}
|
||||
<kbd
|
||||
class="rounded border border-[var(--color-line)] bg-[var(--color-surface-muted)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-ink-muted)]"
|
||||
>
|
||||
{key}
|
||||
</kbd>
|
||||
{/each}
|
||||
{#if item.customized}
|
||||
<button
|
||||
class="rounded p-1 text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
||||
title="Reset to default"
|
||||
aria-label="Reset to default"
|
||||
onclick={() => {
|
||||
resetHotkey(item.id);
|
||||
hotkeyVersion++;
|
||||
}}
|
||||
>
|
||||
<Icon icon="ph:arrow-counter-clockwise" class="text-xs" />
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="rounded p-1 text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
||||
title="Change shortcut"
|
||||
aria-label="Change shortcut"
|
||||
onclick={() => (editingId = item.id)}
|
||||
>
|
||||
<Icon icon="ph:pencil-simple" class="text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#each staticHotkeyGroups as group}
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-[var(--color-ink-muted)]">
|
||||
{group.title}
|
||||
</span>
|
||||
<div class="flex flex-col divide-y divide-[var(--color-line)] rounded-md border border-[var(--color-line)]">
|
||||
{#each group.items as item}
|
||||
<div class="flex items-center justify-between gap-4 px-3 py-2">
|
||||
<span class="text-xs">{item.label}</span>
|
||||
<span class="flex shrink-0 items-center gap-1">
|
||||
{#each item.keys as key}
|
||||
<kbd
|
||||
class="rounded border border-[var(--color-line)] bg-[var(--color-surface-muted)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-ink-muted)]"
|
||||
>
|
||||
{key}
|
||||
</kbd>
|
||||
{/each}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -569,7 +766,8 @@
|
||||
section === "about" ||
|
||||
section === "appearance" ||
|
||||
section === "accessibility" ||
|
||||
section === "lsp"}
|
||||
section === "lsp" ||
|
||||
section === "hotkeys"}
|
||||
<button
|
||||
class="rounded-md px-3 py-1.5 text-xs text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={onclose}
|
||||
|
||||
@@ -18,17 +18,48 @@ export function wrapSelection(
|
||||
placeholder = "",
|
||||
) {
|
||||
if (!view) return;
|
||||
const selection = view.state.selection.main;
|
||||
const selected = view.state.doc.sliceString(selection.from, selection.to);
|
||||
const body = selected || placeholder;
|
||||
const { state } = view;
|
||||
const selection = state.selection.main;
|
||||
const selected = state.doc.sliceString(selection.from, selection.to);
|
||||
|
||||
view.dispatch({
|
||||
changes: { from: selection.from, to: selection.to, insert: prefix + body + suffix },
|
||||
selection: {
|
||||
anchor: selection.from + prefix.length,
|
||||
head: selection.from + prefix.length + body.length,
|
||||
},
|
||||
});
|
||||
const innerWrapped =
|
||||
selected.length >= prefix.length + suffix.length &&
|
||||
selected.startsWith(prefix) &&
|
||||
selected.endsWith(suffix);
|
||||
|
||||
const before = state.doc.sliceString(
|
||||
Math.max(0, selection.from - prefix.length),
|
||||
selection.from,
|
||||
);
|
||||
const after = state.doc.sliceString(
|
||||
selection.to,
|
||||
Math.min(state.doc.length, selection.to + suffix.length),
|
||||
);
|
||||
const outerWrapped = before === prefix && after === suffix;
|
||||
|
||||
if (innerWrapped) {
|
||||
const inner = selected.slice(prefix.length, selected.length - suffix.length);
|
||||
view.dispatch({
|
||||
changes: { from: selection.from, to: selection.to, insert: inner },
|
||||
selection: { anchor: selection.from, head: selection.from + inner.length },
|
||||
});
|
||||
} else if (outerWrapped) {
|
||||
const from = selection.from - prefix.length;
|
||||
const to = selection.to + suffix.length;
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: selected },
|
||||
selection: { anchor: from, head: from + selected.length },
|
||||
});
|
||||
} else {
|
||||
const body = selected || placeholder;
|
||||
view.dispatch({
|
||||
changes: { from: selection.from, to: selection.to, insert: prefix + body + suffix },
|
||||
selection: {
|
||||
anchor: selection.from + prefix.length,
|
||||
head: selection.from + prefix.length + body.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
view.focus();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ const palette: Record<"light" | "dark", ThemeColors> = {
|
||||
background: "#16181d",
|
||||
surface: "#1d2026",
|
||||
text: "#eef0f4",
|
||||
selection: "#2f3a52",
|
||||
selection: "#3f5a91",
|
||||
activeLine: "#1d2026",
|
||||
cursor: "#6b93ff",
|
||||
border: "#2f343d",
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import hotkeys from "hotkeys-js";
|
||||
|
||||
// The default filter ignores any contenteditable target, which silently
|
||||
// blocks every shortcut while the CodeMirror editor (contenteditable) has
|
||||
// focus — exactly when these shortcuts are meant to fire. Only keep the
|
||||
// exclusion for classic form fields (rename dialogs, prompts, etc).
|
||||
hotkeys.filter = (event: KeyboardEvent) => {
|
||||
const target = (event.target as HTMLElement | null) ?? null;
|
||||
const tagName = target?.tagName;
|
||||
return tagName !== "INPUT" && tagName !== "TEXTAREA" && tagName !== "SELECT";
|
||||
};
|
||||
|
||||
export interface HotkeyDef {
|
||||
id: string;
|
||||
group: string;
|
||||
label: string;
|
||||
defaultKeys: string;
|
||||
}
|
||||
|
||||
export const HOTKEY_DEFS: HotkeyDef[] = [
|
||||
{ id: "save", group: "Editor", label: "Save and compile", defaultKeys: "command+s,ctrl+s" },
|
||||
{ id: "undo", group: "Editor", label: "Undo", defaultKeys: "command+z,ctrl+z" },
|
||||
{
|
||||
id: "redo",
|
||||
group: "Editor",
|
||||
label: "Redo",
|
||||
defaultKeys: "command+shift+z,ctrl+shift+z,ctrl+y",
|
||||
},
|
||||
{
|
||||
id: "toggleSidebar",
|
||||
group: "Editor",
|
||||
label: "Toggle file sidebar",
|
||||
defaultKeys: "command+shift+b,ctrl+shift+b",
|
||||
},
|
||||
{ id: "bold", group: "Formatting", label: "Bold (toggle)", defaultKeys: "command+b,ctrl+b" },
|
||||
{ id: "italic", group: "Formatting", label: "Italic (toggle)", defaultKeys: "command+i,ctrl+i" },
|
||||
{
|
||||
id: "underline",
|
||||
group: "Formatting",
|
||||
label: "Underline (toggle)",
|
||||
defaultKeys: "command+u,ctrl+u",
|
||||
},
|
||||
{
|
||||
id: "strikethrough",
|
||||
group: "Formatting",
|
||||
label: "Strikethrough (toggle)",
|
||||
defaultKeys: "command+shift+x,ctrl+shift+x",
|
||||
},
|
||||
{ id: "link", group: "Formatting", label: "Insert link", defaultKeys: "command+k,ctrl+k" },
|
||||
{
|
||||
id: "numberedList",
|
||||
group: "Formatting",
|
||||
label: "Numbered list",
|
||||
defaultKeys: "command+shift+7,ctrl+shift+7",
|
||||
},
|
||||
{
|
||||
id: "bulletedList",
|
||||
group: "Formatting",
|
||||
label: "Bulleted list",
|
||||
defaultKeys: "command+shift+8,ctrl+shift+8",
|
||||
},
|
||||
...[1, 2, 3, 4, 5, 6].map((level) => ({
|
||||
id: `heading${level}`,
|
||||
group: "Formatting",
|
||||
label: `Heading level ${level}`,
|
||||
defaultKeys: `command+alt+${level},ctrl+alt+${level}`,
|
||||
})),
|
||||
];
|
||||
|
||||
const STORAGE_KEY = "hotkey-overrides";
|
||||
|
||||
function loadOverrides(): Record<string, string> {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveOverrides(overrides: Record<string, string>) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides));
|
||||
}
|
||||
|
||||
export function keysFor(id: string): string {
|
||||
const def = HOTKEY_DEFS.find((entry) => entry.id === id);
|
||||
const overrides = loadOverrides();
|
||||
return overrides[id] ?? def?.defaultKeys ?? "";
|
||||
}
|
||||
|
||||
export function isCustomized(id: string): boolean {
|
||||
return id in loadOverrides();
|
||||
}
|
||||
|
||||
const registered = new Map<string, { keys: string; handler: (event: KeyboardEvent) => void }>();
|
||||
|
||||
export function registerHotkey(id: string, handler: (event: KeyboardEvent) => void) {
|
||||
const keys = keysFor(id);
|
||||
if (!keys) return;
|
||||
hotkeys(keys, handler);
|
||||
registered.set(id, { keys, handler });
|
||||
}
|
||||
|
||||
export function unregisterAll() {
|
||||
for (const { keys } of registered.values()) hotkeys.unbind(keys);
|
||||
registered.clear();
|
||||
}
|
||||
|
||||
export function rebindHotkey(id: string, newKeys: string) {
|
||||
const entry = registered.get(id);
|
||||
if (!entry) return;
|
||||
|
||||
hotkeys.unbind(entry.keys);
|
||||
hotkeys(newKeys, entry.handler);
|
||||
registered.set(id, { keys: newKeys, handler: entry.handler });
|
||||
|
||||
const def = HOTKEY_DEFS.find((item) => item.id === id);
|
||||
const overrides = loadOverrides();
|
||||
if (def && def.defaultKeys === newKeys) {
|
||||
delete overrides[id];
|
||||
} else {
|
||||
overrides[id] = newKeys;
|
||||
}
|
||||
saveOverrides(overrides);
|
||||
}
|
||||
|
||||
export function resetHotkey(id: string) {
|
||||
const def = HOTKEY_DEFS.find((entry) => entry.id === id);
|
||||
if (def) rebindHotkey(id, def.defaultKeys);
|
||||
}
|
||||
|
||||
const NAMED_KEYS: Record<string, string> = {
|
||||
" ": "space",
|
||||
Escape: "esc",
|
||||
ArrowUp: "up",
|
||||
ArrowDown: "down",
|
||||
ArrowLeft: "left",
|
||||
ArrowRight: "right",
|
||||
};
|
||||
|
||||
export function comboFromEvent(event: KeyboardEvent): string | null {
|
||||
if (["Control", "Shift", "Alt", "Meta"].includes(event.key)) return null;
|
||||
|
||||
const parts: string[] = [];
|
||||
if (event.ctrlKey) parts.push("ctrl");
|
||||
if (event.metaKey) parts.push("command");
|
||||
if (event.altKey) parts.push("alt");
|
||||
if (event.shiftKey) parts.push("shift");
|
||||
|
||||
const key = event.key;
|
||||
const mainKey = key.length === 1 ? key.toLowerCase() : (NAMED_KEYS[key] ?? key.toLowerCase());
|
||||
parts.push(mainKey);
|
||||
|
||||
return parts.join("+");
|
||||
}
|
||||
+63
-10
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { registerHotkey, unregisterAll } from "$lib/ts/hotkeys";
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { writeImage, writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { Image } from "@tauri-apps/api/image";
|
||||
@@ -24,7 +25,13 @@
|
||||
import PageSettingsModal from "$lib/components/PageSettingsModal.svelte";
|
||||
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { insertText, redoEdit, undoEdit } from "$lib/ts/editor-actions";
|
||||
import {
|
||||
insertText,
|
||||
prefixLines,
|
||||
redoEdit,
|
||||
undoEdit,
|
||||
wrapSelection,
|
||||
} from "$lib/ts/editor-actions";
|
||||
|
||||
import * as api from "$lib/ts/api";
|
||||
import type { BrowseEntry, CloudFile, CloudFolder } from "$lib/ts/api";
|
||||
@@ -446,13 +453,6 @@
|
||||
setStatus("Conflicts resolved and uploaded");
|
||||
});
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === "s") {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor") saveAndCompile();
|
||||
}
|
||||
}
|
||||
|
||||
let dropActive = $state(false);
|
||||
|
||||
function dropDestination():
|
||||
@@ -528,9 +528,64 @@
|
||||
}
|
||||
});
|
||||
|
||||
registerHotkey("save", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor") saveAndCompile();
|
||||
});
|
||||
registerHotkey("undo", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor") undoEdit(editorView);
|
||||
});
|
||||
registerHotkey("redo", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor") redoEdit(editorView);
|
||||
});
|
||||
registerHotkey("toggleSidebar", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor" && !app.target?.standalone) toggleSidebar();
|
||||
});
|
||||
registerHotkey("bold", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor") wrapSelection(editorView, "*", "*", "bold");
|
||||
});
|
||||
registerHotkey("italic", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor") wrapSelection(editorView, "_", "_", "italic");
|
||||
});
|
||||
registerHotkey("underline", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor")
|
||||
wrapSelection(editorView, "#underline[", "]", "underlined");
|
||||
});
|
||||
registerHotkey("strikethrough", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor")
|
||||
wrapSelection(editorView, "#strike[", "]", "struck through");
|
||||
});
|
||||
registerHotkey("link", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor") insertText(editorView, '#link("https://")[text]');
|
||||
});
|
||||
registerHotkey("numberedList", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor") prefixLines(editorView, "+ ", "Numbered item");
|
||||
});
|
||||
registerHotkey("bulletedList", (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor") prefixLines(editorView, "- ", "List item");
|
||||
});
|
||||
for (const level of [1, 2, 3, 4, 5, 6]) {
|
||||
registerHotkey(`heading${level}`, (event) => {
|
||||
event.preventDefault();
|
||||
if (app.view === "editor")
|
||||
prefixLines(editorView, "=".repeat(level) + " ", "Heading");
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
pending.then((unlisten) => unlisten());
|
||||
downloads.then((unlisten) => unlisten());
|
||||
unregisterAll();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -548,7 +603,6 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
{#snippet statusBadge(
|
||||
icon: string,
|
||||
@@ -956,7 +1010,6 @@
|
||||
scheduleCompile();
|
||||
scheduleAutosave();
|
||||
}}
|
||||
onsave={saveAndCompile}
|
||||
onlspstatus={(status) => (app.lspStatus = status)}
|
||||
onready={(view) => (editorView = view)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user