mirror of
https://github.com/immich-app/immich.git
synced 2025-01-27 22:22:45 +01:00
feat(web): scrollable context menus
This commit is contained in:
parent
971ba63447
commit
e628e6a807
15 changed files with 42 additions and 44 deletions
web/src
lib
actions
components
album-page
asset-viewer
faces-page
memory-page
shared-components/context-menu
sharedlinks-page
routes
(user)
archive/[[photos=photos]]/[[assetId=id]]
partners/[userId]/[[photos=photos]]/[[assetId=id]]
people/[personId]/[[photos=photos]]/[[assetId=id]]
search/[[photos=photos]]/[[assetId=id]]
admin/library-management
|
@ -46,7 +46,11 @@ export const contextMenuNavigation: Action<HTMLElement, Options> = (node, option
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveSelection = async (direction: 'up' | 'down', event: KeyboardEvent) => {
|
const moveSelection = async (direction: 'up' | 'down', event: KeyboardEvent) => {
|
||||||
const { selectionChanged, container, openDropdown } = options;
|
const { selectionChanged, container, openDropdown, isOpen } = options;
|
||||||
|
if (!isOpen) {
|
||||||
|
// reset the scroll position before opening the menu
|
||||||
|
container?.scrollTo({ top: 0 });
|
||||||
|
}
|
||||||
if (openDropdown) {
|
if (openDropdown) {
|
||||||
openDropdown(event);
|
openDropdown(event);
|
||||||
await tick();
|
await tick();
|
||||||
|
|
|
@ -109,7 +109,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if isOwned}
|
{#if isOwned}
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}>
|
<ButtonContextMenu
|
||||||
|
icon={mdiDotsVertical}
|
||||||
|
size="20"
|
||||||
|
title={$t('options')}
|
||||||
|
direction="right"
|
||||||
|
align="top-left"
|
||||||
|
>
|
||||||
{#if role === AlbumUserRole.Viewer}
|
{#if role === AlbumUserRole.Viewer}
|
||||||
<MenuOption onClick={() => handleSetReadonly(user, AlbumUserRole.Editor)} text={$t('allow_edits')} />
|
<MenuOption onClick={() => handleSetReadonly(user, AlbumUserRole.Editor)} text={$t('allow_edits')} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -186,13 +186,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||||
<div class="mr-4">
|
<div class="mr-4">
|
||||||
<ButtonContextMenu
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('comment_options')} size="16">
|
||||||
icon={mdiDotsVertical}
|
|
||||||
title={$t('comment_options')}
|
|
||||||
align="top-right"
|
|
||||||
direction="left"
|
|
||||||
size="16"
|
|
||||||
>
|
|
||||||
<MenuOption
|
<MenuOption
|
||||||
activeColor="bg-red-200"
|
activeColor="bg-red-200"
|
||||||
icon={mdiDeleteOutline}
|
icon={mdiDeleteOutline}
|
||||||
|
@ -239,13 +233,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||||
<div class="mr-4">
|
<div class="mr-4">
|
||||||
<ButtonContextMenu
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('reaction_options')} size="16">
|
||||||
icon={mdiDotsVertical}
|
|
||||||
title={$t('reaction_options')}
|
|
||||||
align="top-right"
|
|
||||||
direction="left"
|
|
||||||
size="16"
|
|
||||||
>
|
|
||||||
<MenuOption
|
<MenuOption
|
||||||
activeColor="bg-red-200"
|
activeColor="bg-red-200"
|
||||||
icon={mdiDeleteOutline}
|
icon={mdiDeleteOutline}
|
||||||
|
|
|
@ -128,7 +128,7 @@
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<DeleteAction {asset} {onAction} />
|
<DeleteAction {asset} {onAction} />
|
||||||
|
|
||||||
<ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}>
|
<ButtonContextMenu color="opaque" title={$t('more')} icon={mdiDotsVertical}>
|
||||||
{#if showSlideshow}
|
{#if showSlideshow}
|
||||||
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
|
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -67,6 +67,8 @@
|
||||||
size="20"
|
size="20"
|
||||||
icon={mdiDotsVertical}
|
icon={mdiDotsVertical}
|
||||||
title={$t('show_person_options')}
|
title={$t('show_person_options')}
|
||||||
|
direction="right"
|
||||||
|
align="top-left"
|
||||||
>
|
>
|
||||||
<MenuOption onClick={onHidePerson} icon={mdiEyeOffOutline} text={$t('hide_person')} />
|
<MenuOption onClick={onHidePerson} icon={mdiEyeOffOutline} text={$t('hide_person')} />
|
||||||
<MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
|
<MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
|
||||||
|
|
|
@ -237,7 +237,7 @@
|
||||||
|
|
||||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={handleUpdate} />
|
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={handleUpdate} />
|
||||||
|
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
|
|
|
@ -20,11 +20,11 @@
|
||||||
/**
|
/**
|
||||||
* The alignment of the context menu relative to the button.
|
* The alignment of the context menu relative to the button.
|
||||||
*/
|
*/
|
||||||
export let align: Align = 'top-left';
|
export let align: Align = 'top-right';
|
||||||
/**
|
/**
|
||||||
* The direction in which the context menu should open.
|
* The direction in which the context menu should open.
|
||||||
*/
|
*/
|
||||||
export let direction: 'left' | 'right' = 'right';
|
export let direction: 'left' | 'right' = 'left';
|
||||||
export let color: Color = 'transparent';
|
export let color: Color = 'transparent';
|
||||||
export let size: string | undefined = undefined;
|
export let size: string | undefined = undefined;
|
||||||
export let padding: Padding | undefined = undefined;
|
export let padding: Padding | undefined = undefined;
|
||||||
|
|
|
@ -18,25 +18,26 @@
|
||||||
let left: number;
|
let left: number;
|
||||||
let top: number;
|
let top: number;
|
||||||
|
|
||||||
// We need to bind clientHeight since the bounding box may return a height
|
|
||||||
// of zero when starting the 'slide' animation.
|
|
||||||
let height: number;
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (menuElement) {
|
if (menuElement) {
|
||||||
const rect = menuElement.getBoundingClientRect();
|
const rect = menuElement.getBoundingClientRect();
|
||||||
const directionWidth = direction === 'left' ? rect.width : 0;
|
const directionWidth = direction === 'left' ? rect.width : 0;
|
||||||
const menuHeight = Math.min(menuElement.clientHeight, height) || 0;
|
const menuHeight = menuElement.clientHeight || 0;
|
||||||
|
|
||||||
left = Math.min(window.innerWidth - rect.width, x - directionWidth);
|
const calcLeft = Math.min(window.innerWidth - rect.width, x - directionWidth);
|
||||||
|
left = Math.max(0, calcLeft);
|
||||||
top = Math.min(window.innerHeight - menuHeight, y);
|
top = Math.min(window.innerHeight - menuHeight, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:clientHeight={height}
|
class="fixed z-10 overflow-hidden rounded-lg duration-[250ms] ease-in {isVisible
|
||||||
class="fixed z-10 min-w-[200px] w-max max-w-[300px] overflow-hidden rounded-lg shadow-lg"
|
? 'shadow-lg transition-shadow'
|
||||||
|
: 'shadow-none transition-none'}"
|
||||||
|
class:shadow-none={!isVisible}
|
||||||
|
class:shadow-lg={isVisible}
|
||||||
|
class:transition-none={!isVisible}
|
||||||
style:left="{left}px"
|
style:left="{left}px"
|
||||||
style:top="{top}px"
|
style:top="{top}px"
|
||||||
transition:slide={{ duration: 250, easing: quintOut }}
|
transition:slide={{ duration: 250, easing: quintOut }}
|
||||||
|
@ -48,9 +49,9 @@
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
aria-labelledby={ariaLabelledBy}
|
aria-labelledby={ariaLabelledBy}
|
||||||
bind:this={menuElement}
|
bind:this={menuElement}
|
||||||
class:max-h-[100vh]={isVisible}
|
class="flex flex-col transition-all duration-[250ms] ease-in-out outline-none immich-scrollbar bg-slate-100 relative min-w-[200px] max-w-[200px] sm:max-w-[256px] rounded-lg {isVisible
|
||||||
class:max-h-0={!isVisible}
|
? 'translate-x-0 max-h-dvh overflow-y-auto'
|
||||||
class="flex flex-col transition-all duration-[250ms] ease-in-out outline-none"
|
: `${direction === 'left' ? 'translate-x-28' : '-translate-x-28'} max-h-0 overflow-y-hidden`}"
|
||||||
role="menu"
|
role="menu"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
|
|
|
@ -33,7 +33,9 @@
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<Icon path={icon} ariaHidden={true} size="18" />
|
<div class="flex-none">
|
||||||
|
<Icon path={icon} ariaHidden={true} size="18" />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
{text}
|
{text}
|
||||||
|
|
|
@ -107,6 +107,8 @@
|
||||||
size="24"
|
size="24"
|
||||||
padding="3"
|
padding="3"
|
||||||
hideContent
|
hideContent
|
||||||
|
direction="right"
|
||||||
|
align="top-left"
|
||||||
>
|
>
|
||||||
<SharedLinkEdit menuItem {onEdit} />
|
<SharedLinkEdit menuItem {onEdit} />
|
||||||
<SharedLinkCopy menuItem {link} />
|
<SharedLinkCopy menuItem {link} />
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
<AddToAlbum shared />
|
<AddToAlbum shared />
|
||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
|
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
{#if $isMultiSelectState}
|
{#if $isMultiSelectState}
|
||||||
<AssetSelectControlBar assets={$selectedAssets} clearSelect={clearMultiselect}>
|
<AssetSelectControlBar assets={$selectedAssets} clearSelect={clearMultiselect}>
|
||||||
<CreateSharedLink />
|
<CreateSharedLink />
|
||||||
<ButtonContextMenu icon={mdiPlus} title={$t('add')}>
|
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||||
<AddToAlbum />
|
<AddToAlbum />
|
||||||
<AddToAlbum shared />
|
<AddToAlbum shared />
|
||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
|
|
|
@ -385,7 +385,7 @@
|
||||||
<AddToAlbum shared />
|
<AddToAlbum shared />
|
||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
|
<DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
|
||||||
<MenuOption
|
<MenuOption
|
||||||
icon={mdiAccountMultipleCheckOutline}
|
icon={mdiAccountMultipleCheckOutline}
|
||||||
|
|
|
@ -235,7 +235,7 @@
|
||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} />
|
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} />
|
||||||
|
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
|
|
|
@ -285,14 +285,7 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class=" text-ellipsis px-4 text-sm">
|
<td class=" text-ellipsis px-4 text-sm">
|
||||||
<ButtonContextMenu
|
<ButtonContextMenu color="primary" size="16" icon={mdiDotsVertical} title={$t('library_options')}>
|
||||||
align="top-right"
|
|
||||||
direction="left"
|
|
||||||
color="primary"
|
|
||||||
size="16"
|
|
||||||
icon={mdiDotsVertical}
|
|
||||||
title={$t('library_options')}
|
|
||||||
>
|
|
||||||
<MenuOption onClick={() => onScanClicked(library)} text={$t('scan_library')} />
|
<MenuOption onClick={() => onScanClicked(library)} text={$t('scan_library')} />
|
||||||
<hr />
|
<hr />
|
||||||
<MenuOption onClick={() => onRenameClicked(index)} text={$t('rename')} />
|
<MenuOption onClick={() => onRenameClicked(index)} text={$t('rename')} />
|
||||||
|
|
Loading…
Reference in a new issue