diff options
Diffstat (limited to 'packages/frontend/src/components/MkMenu.vue')
| -rw-r--r-- | packages/frontend/src/components/MkMenu.vue | 134 |
1 files changed, 97 insertions, 37 deletions
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index b0a1b80210..c2fc967e1d 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.center]: align === 'center', [$style.big]: big, [$style.asDrawer]: asDrawer, + [$style.widthSpecified]: width != null, }" @focusin.passive.stop="() => {}" > @@ -29,12 +30,19 @@ SPDX-License-Identifier: AGPL-3.0-only > <template v-for="item in (items2 ?? [])"> <div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div> - <span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]"> - <span style="opacity: 0.7;">{{ item.text }}</span> - </span> + + <div v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label]"> + <span>{{ item.text }}</span> + </div> + <span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]"> <span><MkEllipsis/></span> </span> + + <div v-else-if="item.type === 'component'" role="menuitem" tabindex="-1" :class="[$style.componentItem]"> + <component :is="item.component" v-bind="item.props"/> + </div> + <MkA v-else-if="item.type === 'link'" role="menuitem" @@ -48,10 +56,14 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> + <div :class="$style.item_content_text"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </MkA> + <a v-else-if="item.type === 'a'" role="menuitem" @@ -67,10 +79,14 @@ SPDX-License-Identifier: AGPL-3.0-only > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> + <div :class="$style.item_content_text"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </a> + <button v-else-if="item.type === 'user'" role="menuitem" @@ -85,6 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </button> + <button v-else-if="item.type === 'switch'" role="menuitemcheckbox" @@ -98,10 +115,14 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> <div :class="$style.item_content"> - <span :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">{{ item.text }}</span> + <div :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> </div> </button> + <button v-else-if="item.type === 'radio'" role="menuitem" @@ -114,10 +135,14 @@ SPDX-License-Identifier: AGPL-3.0-only > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> <div :class="$style.item_content"> - <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> + <div :class="$style.item_content_text" style="pointer-events: none;"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> </div> </button> + <button v-else-if="item.type === 'radioOption'" role="menuitemradio" @@ -131,9 +156,13 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span> </div> <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> + <div :class="$style.item_content_text"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> </div> </button> + <button v-else-if="item.type === 'parent'" role="menuitem" @@ -145,12 +174,17 @@ SPDX-License-Identifier: AGPL-3.0-only > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> <div :class="$style.item_content"> - <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> + <div :class="$style.item_content_text" style="pointer-events: none;"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> </div> </button> + <button - v-else role="menuitem" + v-else + role="menuitem" tabindex="0" :class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]" @click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)" @@ -160,11 +194,15 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> + <div :class="$style.item_content_text"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </button> </template> + <span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]"> <span>{{ i18n.ts.none }}</span> </span> @@ -176,15 +214,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue'; +import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, unref, watch, shallowRef } from 'vue'; +import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js'; +import type { Keymap } from '@/utility/hotkey.js'; import MkSwitchButton from '@/components/MkSwitch.button.vue'; -import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { isTouchUsing } from '@/scripts/touch.js'; -import { type Keymap } from '@/scripts/hotkey.js'; -import { isFocusable } from '@/scripts/focus.js'; -import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; +import { isTouchUsing } from '@/utility/touch.js'; +import { isFocusable } from '@/utility/focus.js'; +import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js'; const childrenCache = new WeakMap<MenuParent, MenuItem[]>(); </script> @@ -209,11 +247,11 @@ const big = isTouchUsing; const isNestingMenu = inject<boolean>('isNestingMenu', false); -const itemsEl = shallowRef<HTMLElement>(); +const itemsEl = useTemplateRef('itemsEl'); const items2 = ref<InnerMenuItem[]>(); -const child = shallowRef<InstanceType<typeof XChild>>(); +const child = useTemplateRef('child'); const keymap = { 'up|k|shift+tab': { @@ -254,7 +292,7 @@ watch(() => props.items, () => { }); const childMenu = ref<MenuItem[] | null>(); -const childTarget = shallowRef<HTMLElement | null>(); +const childTarget = shallowRef<HTMLElement>(); function closeChild() { childMenu.value = null; @@ -355,10 +393,10 @@ function switchItem(item: MenuSwitch & { ref: any }) { function focusUp() { if (disposed) return; - if (!itemsEl.value?.contains(document.activeElement)) return; + if (!itemsEl.value?.contains(window.document.activeElement)) return; const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable); - const activeIndex = focusableElements.findIndex(el => el === document.activeElement); + const activeIndex = focusableElements.findIndex(el => el === window.document.activeElement); const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1); const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value; @@ -367,10 +405,10 @@ function focusUp() { function focusDown() { if (disposed) return; - if (!itemsEl.value?.contains(document.activeElement)) return; + if (!itemsEl.value?.contains(window.document.activeElement)) return; const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable); - const activeIndex = focusableElements.findIndex(el => el === document.activeElement); + const activeIndex = focusableElements.findIndex(el => el === window.document.activeElement); const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0; const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value; @@ -397,9 +435,9 @@ const onGlobalMousedown = (ev: MouseEvent) => { const setupHandlers = () => { if (!isNestingMenu) { - document.addEventListener('focusin', onGlobalFocusin, { passive: true }); + window.document.addEventListener('focusin', onGlobalFocusin, { passive: true }); } - document.addEventListener('mousedown', onGlobalMousedown, { passive: true }); + window.document.addEventListener('mousedown', onGlobalMousedown, { passive: true }); }; let disposed = false; @@ -407,9 +445,9 @@ let disposed = false; const disposeHandlers = () => { disposed = true; if (!isNestingMenu) { - document.removeEventListener('focusin', onGlobalFocusin); + window.document.removeEventListener('focusin', onGlobalFocusin); } - document.removeEventListener('mousedown', onGlobalMousedown); + window.document.removeEventListener('mousedown', onGlobalMousedown); }; onMounted(() => { @@ -435,6 +473,12 @@ onBeforeUnmount(() => { } } + &:not(.asDrawer):not(.widthSpecified) { + > .menu { + max-width: 400px; + } + } + &.big:not(.asDrawer) { > .menu { min-width: 230px; @@ -558,11 +602,11 @@ onBeforeUnmount(() => { } &.danger { - --menuFg: #ff2a2a; + --menuFg: var(--MI_THEME-error); --menuHoverFg: #fff; - --menuHoverBg: #ff4242; + --menuHoverBg: var(--MI_THEME-error); --menuActiveFg: #fff; - --menuActiveBg: #d42e2e; + --menuActiveBg: hsl(from var(--MI_THEME-error) h s calc(l - 10)); } &.radio { @@ -575,12 +619,6 @@ onBeforeUnmount(() => { --menuActiveBg: var(--MI_THEME-accentedBg); } - &.label { - pointer-events: none; - font-size: 0.7em; - padding-bottom: 4px; - } - &.pending { pointer-events: none; opacity: 0.7; @@ -604,10 +642,19 @@ onBeforeUnmount(() => { .item_content_text { max-width: calc(100vw - 4rem); +} + +.item_content_text_title { text-overflow: ellipsis; overflow: hidden; } +.item_content_text_caption { + text-wrap: auto; + font-size: 85%; + opacity: 0.7; +} + .switchButton { margin-left: -2px; --height: 1.35em; @@ -641,6 +688,19 @@ onBeforeUnmount(() => { font-size: 12px; } +.label { + position: relative; + padding: 6px 16px; + box-sizing: border-box; + white-space: nowrap; + font-size: 0.7em; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + opacity: 0.7; + pointer-events: none; +} + .divider { margin: 8px 0; border-top: solid 0.5px var(--MI_THEME-divider); |