mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
fix(web): escape shortcut handling (#26096)
This commit is contained in:
parent
e54678e0d6
commit
913904f418
5 changed files with 17 additions and 119 deletions
|
|
@ -1,112 +1,9 @@
|
||||||
import type { ActionReturn } from 'svelte/action';
|
export {
|
||||||
|
matchesShortcut,
|
||||||
export type Shortcut = {
|
shortcut,
|
||||||
key: string;
|
shortcutLabel,
|
||||||
alt?: boolean;
|
shortcuts,
|
||||||
ctrl?: boolean;
|
shouldIgnoreEvent,
|
||||||
shift?: boolean;
|
type Shortcut,
|
||||||
meta?: boolean;
|
type ShortcutOptions,
|
||||||
};
|
} from '@immich/ui';
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,6 @@
|
||||||
|
|
||||||
<svelte:document
|
<svelte:document
|
||||||
use:shortcuts={[
|
use:shortcuts={[
|
||||||
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
|
|
||||||
{ shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input?.select() },
|
{ shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input?.select() },
|
||||||
{ shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
|
{ shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
|
||||||
]}
|
]}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,10 @@
|
||||||
|
|
||||||
const handleTagAssets = async () => {
|
const handleTagAssets = async () => {
|
||||||
const assets = [...getOwnedAssets()];
|
const assets = [...getOwnedAssets()];
|
||||||
await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
|
const didUpdate = await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
|
||||||
clearSelect();
|
if (didUpdate) {
|
||||||
|
clearSelect();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
import { modalManager } from '@immich/ui';
|
import { isModalOpen, modalManager } from '@immich/ui';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
timelineManager: TimelineManager;
|
timelineManager: TimelineManager;
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const shortcutList = $derived.by(() => {
|
const shortcutList = $derived.by(() => {
|
||||||
if (searchStore.isSearchEnabled || $showAssetViewer) {
|
if (searchStore.isSearchEnabled || $showAssetViewer || isModalOpen()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
import Combobox, { type ComboBoxOption } from '../components/shared-components/combobox.svelte';
|
import Combobox, { type ComboBoxOption } from '../components/shared-components/combobox.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: (updated?: boolean) => void;
|
||||||
assetIds: string[];
|
assetIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
const updatedIds = await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false });
|
const updatedIds = await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false });
|
||||||
eventManager.emit('AssetsTag', updatedIds);
|
eventManager.emit('AssetsTag', updatedIds);
|
||||||
onClose();
|
onClose(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = async (option?: ComboBoxOption) => {
|
const handleSelect = async (option?: ComboBoxOption) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue