mirror of
https://github.com/immich-app/immich.git
synced 2026-02-13 04:17:56 +03:00
fix(web): escape shortcut handling (#26096)
This commit is contained in:
@@ -1,112 +1,9 @@
|
||||
import type { ActionReturn } from 'svelte/action';
|
||||
|
||||
export type Shortcut = {
|
||||
key: string;
|
||||
alt?: boolean;
|
||||
ctrl?: boolean;
|
||||
shift?: boolean;
|
||||
meta?: boolean;
|
||||
};
|
||||
|
||||
export type ShortcutOptions<T = HTMLElement> = {
|
||||
shortcut: Shortcut;
|
||||
/** If true, the event handler will not execute if the event comes from an input field */
|
||||
ignoreInputFields?: boolean;
|
||||
onShortcut: (event: KeyboardEvent & { currentTarget: T }) => unknown;
|
||||
preventDefault?: boolean;
|
||||
};
|
||||
|
||||
export const shortcutLabel = (shortcut: Shortcut) => {
|
||||
let label = '';
|
||||
|
||||
if (shortcut.ctrl) {
|
||||
label += 'Ctrl ';
|
||||
}
|
||||
if (shortcut.alt) {
|
||||
label += 'Alt ';
|
||||
}
|
||||
if (shortcut.meta) {
|
||||
label += 'Cmd ';
|
||||
}
|
||||
if (shortcut.shift) {
|
||||
label += '⇧';
|
||||
}
|
||||
label += shortcut.key.toUpperCase();
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
/** Determines whether an event should be ignored. The event will be ignored if:
|
||||
* - The element dispatching the event is not the same as the element which the event listener is attached to
|
||||
* - The element dispatching the event is an input field
|
||||
* - The element dispatching the event is a map canvas
|
||||
*/
|
||||
export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => {
|
||||
if (event.target === event.currentTarget) {
|
||||
return false;
|
||||
}
|
||||
const type = (event.target as HTMLInputElement).type;
|
||||
return (
|
||||
['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type) ||
|
||||
(event.target instanceof HTMLCanvasElement && event.target.classList.contains('maplibregl-canvas'))
|
||||
);
|
||||
};
|
||||
|
||||
export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => {
|
||||
return (
|
||||
shortcut.key.toLowerCase() === event.key.toLowerCase() &&
|
||||
Boolean(shortcut.alt) === event.altKey &&
|
||||
Boolean(shortcut.ctrl) === event.ctrlKey &&
|
||||
Boolean(shortcut.shift) === event.shiftKey &&
|
||||
Boolean(shortcut.meta) === event.metaKey
|
||||
);
|
||||
};
|
||||
|
||||
/** Bind a single keyboard shortcut to node. */
|
||||
export const shortcut = <T extends HTMLElement>(
|
||||
node: T,
|
||||
option: ShortcutOptions<T>,
|
||||
): ActionReturn<ShortcutOptions<T>> => {
|
||||
const { update: shortcutsUpdate, destroy } = shortcuts(node, [option]);
|
||||
|
||||
return {
|
||||
update(newOption) {
|
||||
shortcutsUpdate?.([newOption]);
|
||||
},
|
||||
destroy,
|
||||
};
|
||||
};
|
||||
|
||||
/** Binds multiple keyboard shortcuts to node */
|
||||
export const shortcuts = <T extends HTMLElement>(
|
||||
node: T,
|
||||
options: ShortcutOptions<T>[],
|
||||
): ActionReturn<ShortcutOptions<T>[]> => {
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
const ignoreShortcut = shouldIgnoreEvent(event);
|
||||
for (const { shortcut, onShortcut, ignoreInputFields = true, preventDefault = true } of options) {
|
||||
if (ignoreInputFields && ignoreShortcut) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchesShortcut(event, shortcut)) {
|
||||
if (preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
onShortcut(event as KeyboardEvent & { currentTarget: T });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener('keydown', onKeydown);
|
||||
|
||||
return {
|
||||
update(newOptions) {
|
||||
options = newOptions;
|
||||
},
|
||||
destroy() {
|
||||
node.removeEventListener('keydown', onKeydown);
|
||||
},
|
||||
};
|
||||
};
|
||||
export {
|
||||
matchesShortcut,
|
||||
shortcut,
|
||||
shortcutLabel,
|
||||
shortcuts,
|
||||
shouldIgnoreEvent,
|
||||
type Shortcut,
|
||||
type ShortcutOptions,
|
||||
} from '@immich/ui';
|
||||
|
||||
@@ -242,7 +242,6 @@
|
||||
|
||||
<svelte:document
|
||||
use:shortcuts={[
|
||||
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
|
||||
{ shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input?.select() },
|
||||
{ shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
|
||||
]}
|
||||
|
||||
@@ -20,8 +20,10 @@
|
||||
|
||||
const handleTagAssets = async () => {
|
||||
const assets = [...getOwnedAssets()];
|
||||
await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
|
||||
clearSelect();
|
||||
const didUpdate = await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
|
||||
if (didUpdate) {
|
||||
clearSelect();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { modalManager } from '@immich/ui';
|
||||
import { isModalOpen, modalManager } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
timelineManager: TimelineManager;
|
||||
@@ -142,7 +142,7 @@
|
||||
};
|
||||
|
||||
const shortcutList = $derived.by(() => {
|
||||
if (searchStore.isSearchEnabled || $showAssetViewer) {
|
||||
if (searchStore.isSearchEnabled || $showAssetViewer || isModalOpen()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import Combobox, { type ComboBoxOption } from '../components/shared-components/combobox.svelte';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onClose: (updated?: boolean) => void;
|
||||
assetIds: string[];
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
const updatedIds = await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false });
|
||||
eventManager.emit('AssetsTag', updatedIds);
|
||||
onClose();
|
||||
onClose(true);
|
||||
};
|
||||
|
||||
const handleSelect = async (option?: ComboBoxOption) => {
|
||||
|
||||
Reference in New Issue
Block a user