diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-07-18 05:07:12 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-07-18 05:07:12 +0900 |
| commit | 85ce00adc0d6d9d0e7b9d65931fc9f73b0ee1537 (patch) | |
| tree | 5dfbf0819ba2288e14c52e7a43da225297b1e96d /packages/client/src | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.117.0 (diff) | |
| download | misskey-85ce00adc0d6d9d0e7b9d65931fc9f73b0ee1537.tar.gz misskey-85ce00adc0d6d9d0e7b9d65931fc9f73b0ee1537.tar.bz2 misskey-85ce00adc0d6d9d0e7b9d65931fc9f73b0ee1537.zip | |
Merge branch 'develop'
Diffstat (limited to 'packages/client/src')
21 files changed, 824 insertions, 591 deletions
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index 38f2ee4b36..243aea68c4 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -206,17 +206,16 @@ export async function openAccountMenu(opts: { to: `/@${ $i.username }`, avatar: $i, }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { + type: 'parent', icon: 'fas fa-plus', text: i18n.ts.addAccount, - action: () => { - popupMenu([{ - text: i18n.ts.existingAccount, - action: () => { showSigninDialog(); }, - }, { - text: i18n.ts.createAccount, - action: () => { createAccount(); }, - }], ev.currentTarget ?? ev.target); - }, + children: [{ + text: i18n.ts.existingAccount, + action: () => { showSigninDialog(); }, + }, { + text: i18n.ts.createAccount, + action: () => { createAccount(); }, + }], }, { type: 'link', icon: 'fas fa-users', diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue index c7cf12e8c8..67bf54def8 100644 --- a/packages/client/src/components/global/a.vue +++ b/packages/client/src/components/global/a.vue @@ -50,7 +50,7 @@ function onContextmenu(ev) { icon: 'fas fa-expand-alt', text: i18n.ts.showInPage, action: () => { - router.push(props.to); + router.push(props.to, 'forcePage'); }, }, null, { icon: 'fas fa-external-link-alt', @@ -79,7 +79,7 @@ function popout() { popout_(props.to); } -function nav() { +function nav(ev: MouseEvent) { if (props.behavior === 'browser') { location.href = props.to; return; @@ -93,6 +93,10 @@ function nav() { } } - router.push(props.to); + if (ev.shiftKey) { + return openWindow(); + } + + router.push(props.to, ev.ctrlKey ? 'forcePage' : null); } </script> diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue index c32409ecf4..d9f196f887 100644 --- a/packages/client/src/components/instance-ticker.vue +++ b/packages/client/src/components/instance-ticker.vue @@ -8,6 +8,7 @@ <script lang="ts" setup> import { } from 'vue'; import { instanceName } from '@/config'; +import { instance as Instance } from '@/instance'; const props = defineProps<{ instance?: { @@ -19,7 +20,7 @@ const props = defineProps<{ // if no instance data is given, this is for the local instance const instance = props.instance ?? { - faviconUrl: '/favicon.ico', + faviconUrl: Instance.iconUrl || Instance.faviconUrl || '/favicon.ico', name: instanceName, themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content }; diff --git a/packages/client/src/components/launch-pad.vue b/packages/client/src/components/launch-pad.vue index 4693df2916..7891f61bf1 100644 --- a/packages/client/src/components/launch-pad.vue +++ b/packages/client/src/components/launch-pad.vue @@ -15,20 +15,6 @@ </MkA> </template> </div> - <div class="sub"> - <button v-click-anime class="_button" @click="help"> - <i class="fas fa-question-circle icon"></i> - <div class="text">{{ $ts.help }}</div> - </button> - <MkA v-click-anime to="/about" @click.passive="close()"> - <i class="fas fa-info-circle icon"></i> - <div class="text">{{ $ts.instanceInfo }}</div> - </MkA> - <MkA v-click-anime to="/about-misskey" @click.passive="close()"> - <img src="/static-assets/favicon.png" class="icon"/> - <div class="text">{{ $ts.aboutMisskey }}</div> - </MkA> - </div> </div> </MkModal> </template> @@ -74,28 +60,6 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => function close() { modal.close(); } - -function help(ev: MouseEvent) { - os.popupMenu([{ - type: 'link', - to: '/mfm-cheat-sheet', - text: i18n.ts._mfm.cheatSheet, - icon: 'fas fa-code', - }, { - type: 'link', - to: '/scratchpad', - text: i18n.ts.scratchpad, - icon: 'fas fa-terminal', - }, null, { - text: i18n.ts.document, - icon: 'fas fa-question-circle', - action: () => { - window.open('https://misskey-hub.net/help.html', '_blank'); - }, - }], ev.currentTarget ?? ev.target); - - close(); -} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue index eb889c4888..eaebc3541a 100644 --- a/packages/client/src/components/reactions-viewer.details.vue +++ b/packages/client/src/components/reactions-viewer.details.vue @@ -50,14 +50,14 @@ const emit = defineEmits<{ } > .name { - font-size: 0.9em; + font-size: 1em; } } > .users { flex: 1; min-width: 0; - font-size: 0.9em; + font-size: 0.95em; border-left: solid 0.5px var(--divider); padding-left: 10px; margin-left: 10px; diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue index 5f5d6d42ed..d3a4b5ea92 100644 --- a/packages/client/src/components/ui/button.vue +++ b/packages/client/src/components/ui/button.vue @@ -46,7 +46,7 @@ export default defineComponent({ rounded: { type: Boolean, required: false, - default: false, + default: true, }, inline: { type: Boolean, diff --git a/packages/client/src/components/ui/context-menu.vue b/packages/client/src/components/ui/context-menu.vue index e637d361cf..165c3db462 100644 --- a/packages/client/src/components/ui/context-menu.vue +++ b/packages/client/src/components/ui/context-menu.vue @@ -1,16 +1,16 @@ <template> <transition :name="$store.state.animation ? 'fade' : ''" appear> <div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> - <MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/> + <MkMenu :items="items" :align="'left'" @close="$emit('closed')"/> </div> </transition> </template> <script lang="ts" setup> import { onMounted, onBeforeUnmount } from 'vue'; -import contains from '@/scripts/contains'; import MkMenu from './menu.vue'; import { MenuItem } from './types/menu.vue'; +import contains from '@/scripts/contains'; import * as os from '@/os'; const props = defineProps<{ diff --git a/packages/client/src/components/ui/menu.child.vue b/packages/client/src/components/ui/menu.child.vue new file mode 100644 index 0000000000..b67224d3e1 --- /dev/null +++ b/packages/client/src/components/ui/menu.child.vue @@ -0,0 +1,65 @@ +<template> +<div ref="el" class="sfhdhdhr"> + <MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/> +</div> +</template> + +<script lang="ts" setup> +import { on } from 'events'; +import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'; +import MkMenu from './menu.vue'; +import { MenuItem } from '@/types/menu'; +import * as os from '@/os'; + +const props = defineProps<{ + items: MenuItem[]; + targetElement: HTMLElement; + rootElement: HTMLElement; + width?: number; + viaKeyboard?: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; + (ev: 'actioned'): void; +}>(); + +const el = ref<HTMLElement>(); +const align = 'left'; + +function setPosition() { + const rootRect = props.rootElement.getBoundingClientRect(); + const rect = props.targetElement.getBoundingClientRect(); + const left = props.targetElement.offsetWidth; + const top = (rect.top - rootRect.top) - 8; + el.value.style.left = left + 'px'; + el.value.style.top = top + 'px'; +} + +function onChildClosed(actioned?: boolean) { + if (actioned) { + emit('actioned'); + } else { + emit('closed'); + } +} + +onMounted(() => { + setPosition(); + nextTick(() => { + setPosition(); + }); +}); + +defineExpose({ + checkHit: (ev: MouseEvent) => { + return (ev.target === el.value || el.value.contains(ev.target)); + }, +}); +</script> + +<style lang="scss" scoped> +.sfhdhdhr { + position: absolute; +} +</style> diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue index 6ad63c2ad7..6d1a2cc770 100644 --- a/packages/client/src/components/ui/menu.vue +++ b/packages/client/src/components/ui/menu.vue @@ -1,55 +1,67 @@ <template> -<div - ref="itemsEl" v-hotkey="keymap" - class="rrevdjwt" - :class="{ center: align === 'center', asDrawer }" - :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" - @contextmenu.self="e => e.preventDefault()" -> - <template v-for="(item, i) in items2"> - <div v-if="item === null" class="divider"></div> - <span v-else-if="item.type === 'label'" class="label item"> - <span>{{ item.text }}</span> +<div> + <div + ref="itemsEl" v-hotkey="keymap" + class="rrevdjwt _popup _shadow" + :class="{ center: align === 'center', asDrawer }" + :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" + @contextmenu.self="e => e.preventDefault()" + > + <template v-for="(item, i) in items2"> + <div v-if="item === null" class="divider"></div> + <span v-else-if="item.type === 'label'" class="label item"> + <span>{{ item.text }}</span> + </span> + <span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item"> + <span><MkEllipsis/></span> + </span> + <MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> + <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> + <span>{{ item.text }}</span> + <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> + </MkA> + <a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> + <span>{{ item.text }}</span> + <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> + </a> + <button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> + <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> + </button> + <span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch> + </span> + <button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)"> + <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> + <span>{{ item.text }}</span> + <span class="caret"><i class="fas fa-caret-right fa-fw"></i></span> + </button> + <button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> + <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> + <span>{{ item.text }}</span> + <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> + </button> + </template> + <span v-if="items2.length === 0" class="none item"> + <span>{{ $ts.none }}</span> </span> - <span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item"> - <span><MkEllipsis/></span> - </span> - <MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close()"> - <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> - <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> - <span>{{ item.text }}</span> - <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> - </MkA> - <a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close()"> - <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> - <span>{{ item.text }}</span> - <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> - </a> - <button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)"> - <MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> - <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> - </button> - <span v-else-if="item.type === 'switch'" :tabindex="i" class="item"> - <FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch> - </span> - <button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)"> - <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> - <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> - <span>{{ item.text }}</span> - <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> - </button> - </template> - <span v-if="items2.length === 0" class="none item"> - <span>{{ $ts.none }}</span> - </span> + </div> + <div v-if="childMenu" class="child"> + <XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/> + </div> </div> </template> <script lang="ts" setup> -import { nextTick, onMounted, watch } from 'vue'; +import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue'; import { focusPrev, focusNext } from '@/scripts/focus'; import FormSwitch from '@/components/form/switch.vue'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu'; +import * as os from '@/os'; +const XChild = defineAsyncComponent(() => import('./menu.child.vue')); const props = defineProps<{ items: MenuItem[]; @@ -61,19 +73,23 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (ev: 'close'): void; + (ev: 'close', actioned?: boolean): void; }>(); let itemsEl = $ref<HTMLDivElement>(); let items2: InnerMenuItem[] = $ref([]); +let child = $ref<InstanceType<typeof XChild>>(); + let keymap = $computed(() => ({ 'up|k|shift+tab': focusUp, 'down|j|tab': focusDown, 'esc': close, })); +let childShowingItem = $ref<MenuItem | null>(); + watch(() => props.items, () => { const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined); @@ -93,21 +109,53 @@ watch(() => props.items, () => { immediate: true, }); -onMounted(() => { - if (props.viaKeyboard) { - nextTick(() => { - focusNext(itemsEl.children[0], true, false); - }); +let childMenu = $ref<MenuItem[] | null>(); +let childTarget = $ref<HTMLElement | null>(); + +function closeChild() { + childMenu = null; + childShowingItem = null; +} + +function childActioned() { + closeChild(); + close(true); +} + +function onGlobalMousedown(event: MouseEvent) { + if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return; + if (child && child.checkHit(event)) return; + closeChild(); +} + +let childCloseTimer: null | number = null; +function onItemMouseEnter(item) { + childCloseTimer = window.setTimeout(() => { + closeChild(); + }, 300); +} +function onItemMouseLeave(item) { + if (childCloseTimer) window.clearTimeout(childCloseTimer); +} + +async function showChildren(item: MenuItem, ev: MouseEvent) { + if (props.asDrawer) { + os.popupMenu(item.children, ev.currentTarget ?? ev.target); + close(); + } else { + childTarget = ev.currentTarget ?? ev.target; + childMenu = item.children; + childShowingItem = item; } -}); +} function clicked(fn: MenuAction, ev: MouseEvent) { fn(ev); - close(); + close(true); } -function close() { - emit('close'); +function close(actioned = false) { + emit('close', actioned); } function focusUp() { @@ -117,6 +165,20 @@ function focusUp() { function focusDown() { focusNext(document.activeElement); } + +onMounted(() => { + if (props.viaKeyboard) { + nextTick(() => { + focusNext(itemsEl.children[0], true, false); + }); + } + + document.addEventListener('mousedown', onGlobalMousedown, { passive: true }); +}); + +onBeforeUnmount(() => { + document.removeEventListener('mousedown', onGlobalMousedown); +}); </script> <style lang="scss" scoped> @@ -225,6 +287,25 @@ function focusDown() { opacity: 0.7; } + &.parent { + display: flex; + align-items: center; + cursor: default; + + > .caret { + margin-left: auto; + } + + &.childShowing { + color: var(--accent); + text-decoration: none; + + &:before { + background: var(--accentedBg); + } + } + } + > i { margin-right: 5px; width: 20px; diff --git a/packages/client/src/components/ui/popup-menu.vue b/packages/client/src/components/ui/popup-menu.vue index 2bc7030d77..c29aff45e7 100644 --- a/packages/client/src/components/ui/popup-menu.vue +++ b/packages/client/src/components/ui/popup-menu.vue @@ -1,6 +1,6 @@ <template> <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')"> - <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/> + <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/> </MkModal> </template> diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue index f81bf2fc5b..4c6258d245 100644 --- a/packages/client/src/components/ui/tooltip.vue +++ b/packages/client/src/components/ui/tooltip.vue @@ -12,6 +12,7 @@ <script lang="ts" setup> import { nextTick, onMounted, onUnmounted, ref } from 'vue'; import * as os from '@/os'; +import { calcPopupPosition } from '@/scripts/popup-position'; const props = withDefaults(defineProps<{ showing: boolean; @@ -36,151 +37,20 @@ const emit = defineEmits<{ const el = ref<HTMLElement>(); const zIndex = os.claimZIndex('high'); -const setPosition = () => { - if (el.value == null) return; - - const contentWidth = el.value.offsetWidth; - const contentHeight = el.value.offsetHeight; - - let rect: DOMRect; - - if (props.targetElement) { - rect = props.targetElement.getBoundingClientRect(); - } - - const calcPosWhenTop = () => { - let left: number; - let top: number; - - if (props.targetElement) { - left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); - top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; - } else { - left = props.x; - top = (props.y - contentHeight) - props.innerMargin; - } - - left -= (el.value.offsetWidth / 2); - - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; - } - - return [left, top]; - }; - - const calcPosWhenBottom = () => { - let left: number; - let top: number; - - if (props.targetElement) { - left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); - top = (rect.top + window.pageYOffset + props.targetElement.offsetHeight) + props.innerMargin; - } else { - left = props.x; - top = (props.y) + props.innerMargin; - } - - left -= (el.value.offsetWidth / 2); - - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; - } - - return [left, top]; - }; - - const calcPosWhenLeft = () => { - let left: number; - let top: number; - - if (props.targetElement) { - left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; - top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2); - } else { - left = (props.x - contentWidth) - props.innerMargin; - top = props.y; - } - - top -= (el.value.offsetHeight / 2); - - if (top + contentHeight - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - contentHeight + window.pageYOffset - 1; - } - - return [left, top]; - }; - - const calcPosWhenRight = () => { - let left: number; - let top: number; - - if (props.targetElement) { - left = (rect.left + props.targetElement.offsetWidth + window.pageXOffset) + props.innerMargin; - top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2); - } else { - left = props.x + props.innerMargin; - top = props.y; - } - - top -= (el.value.offsetHeight / 2); - - if (top + contentHeight - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - contentHeight + window.pageYOffset - 1; - } - - return [left, top]; - }; - - const calc = (): { - left: number; - top: number; - transformOrigin: string; - } => { - switch (props.direction) { - case 'top': { - const [left, top] = calcPosWhenTop(); - - // ツールチップを上に向かって表示するスペースがなければ下に向かって出す - if (top - window.pageYOffset < 0) { - const [left, top] = calcPosWhenBottom(); - return { left, top, transformOrigin: 'center top' }; - } - - return { left, top, transformOrigin: 'center bottom' }; - } - - case 'bottom': { - const [left, top] = calcPosWhenBottom(); - // TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す - return { left, top, transformOrigin: 'center top' }; - } - - case 'left': { - const [left, top] = calcPosWhenLeft(); - - // ツールチップを左に向かって表示するスペースがなければ右に向かって出す - if (left - window.pageXOffset < 0) { - const [left, top] = calcPosWhenRight(); - return { left, top, transformOrigin: 'left center' }; - } - - return { left, top, transformOrigin: 'right center' }; - } - - case 'right': { - const [left, top] = calcPosWhenRight(); - // TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す - return { left, top, transformOrigin: 'left center' }; - } - } - }; +function setPosition() { + const data = calcPopupPosition(el.value, { + anchorElement: props.targetElement, + direction: props.direction, + align: 'center', + innerMargin: props.innerMargin, + x: props.x, + y: props.y, + }); - const { left, top, transformOrigin } = calc(); - el.value.style.transformOrigin = transformOrigin; - el.value.style.left = left + 'px'; - el.value.style.top = top + 'px'; -}; + el.value.style.transformOrigin = data.transformOrigin; + el.value.style.left = data.left + 'px'; + el.value.style.top = data.top + 'px'; +} let loopHandler; diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue index d155033824..e259ecdab3 100644 --- a/packages/client/src/components/ui/window.vue +++ b/packages/client/src/components/ui/window.vue @@ -1,6 +1,6 @@ <template> <transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')"> - <div v-if="showing" class="ebkgocck"> + <div v-if="showing" ref="rootEl" class="ebkgocck" :class="{ maximized }"> <div class="body _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown"> <div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu"> <span class="left"> @@ -11,6 +11,8 @@ </span> <span class="right"> <button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button> + <button v-if="canResize && maximized" class="button _button" @click="unMaximize()"><i class="fas fa-window-restore"></i></button> + <button v-else-if="canResize && !maximized" class="button _button" @click="maximize()"><i class="fas fa-window-maximize"></i></button> <button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button> </span> </div> @@ -32,15 +34,16 @@ </transition> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onBeforeUnmount, onMounted, provide } from 'vue'; import contains from '@/scripts/contains'; import * as os from '@/os'; +import { MenuItem } from '@/types/menu'; const minHeight = 50; const minWidth = 250; -function dragListen(fn) { +function dragListen(fn: (ev: MouseEvent) => void) { window.addEventListener('mousemove', fn); window.addEventListener('touchmove', fn); window.addEventListener('mouseleave', dragClear.bind(null, fn)); @@ -56,315 +59,342 @@ function dragClear(fn) { window.removeEventListener('touchend', dragClear); } -export default defineComponent({ - provide: { - inWindow: true, - }, +const props = withDefaults(defineProps<{ + initialWidth?: number; + initialHeight?: number | null; + canResize?: boolean; + closeButton?: boolean; + mini?: boolean; + front?: boolean; + contextmenu?: MenuItem[] | null; + buttonsLeft?: any[]; + buttonsRight?: any[]; +}>(), { + initialWidth: 400, + initialHeight: null, + canResize: false, + closeButton: true, + mini: false, + front: true, + contextmenu: null, + buttonsLeft: () => [], + buttonsRight: () => [], +}); - props: { - initialWidth: { - type: Number, - required: false, - default: 400, - }, - initialHeight: { - type: Number, - required: false, - default: null, - }, - canResize: { - type: Boolean, - required: false, - default: false, - }, - closeButton: { - type: Boolean, - required: false, - default: true, - }, - mini: { - type: Boolean, - required: false, - default: false, - }, - front: { - type: Boolean, - required: false, - default: false, - }, - contextmenu: { - type: Array, - required: false, - }, - buttonsLeft: { - type: Array, - required: false, - default: () => [], - }, - buttonsRight: { - type: Array, - required: false, - default: () => [], - }, - }, +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); - emits: ['closed'], +provide('inWindow', true); - data() { - return { - showing: true, - id: Math.random().toString(), // TODO: UUIDとかにする - }; - }, +let rootEl = $ref<HTMLElement | null>(); +let showing = $ref(true); +let beforeClickedAt = 0; +let maximized = $ref(false); +let unMaximizedTop = ''; +let unMaximizedLeft = ''; +let unMaximizedWidth = ''; +let unMaximizedHeight = ''; - mounted() { - if (this.initialWidth) this.applyTransformWidth(this.initialWidth); - if (this.initialHeight) this.applyTransformHeight(this.initialHeight); +function close() { + showing = false; +} - this.applyTransformTop((window.innerHeight / 2) - (this.$el.offsetHeight / 2)); - this.applyTransformLeft((window.innerWidth / 2) - (this.$el.offsetWidth / 2)); +function onKeydown(evt) { + if (evt.which === 27) { // Esc + evt.preventDefault(); + evt.stopPropagation(); + close(); + } +} - // 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする - this.top(); +function onContextmenu(ev: MouseEvent) { + if (props.contextmenu) { + os.contextMenu(props.contextmenu, ev); + } +} - window.addEventListener('resize', this.onBrowserResize); - }, +// 最前面へ移動 +function top() { + if (rootEl) { + rootEl.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low'); + } +} - unmounted() { - window.removeEventListener('resize', this.onBrowserResize); - }, +function maximize() { + maximized = true; + unMaximizedTop = rootEl.style.top; + unMaximizedLeft = rootEl.style.left; + unMaximizedWidth = rootEl.style.width; + unMaximizedHeight = rootEl.style.height; + rootEl.style.top = '0'; + rootEl.style.left = '0'; + rootEl.style.width = '100%'; + rootEl.style.height = '100%'; +} - methods: { - close() { - this.showing = false; - }, +function unMaximize() { + maximized = false; + rootEl.style.top = unMaximizedTop; + rootEl.style.left = unMaximizedLeft; + rootEl.style.width = unMaximizedWidth; + rootEl.style.height = unMaximizedHeight; +} - onKeydown(evt) { - if (evt.which === 27) { // Esc - evt.preventDefault(); - evt.stopPropagation(); - this.close(); - } - }, +function onBodyMousedown() { + top(); +} - onContextmenu(ev: MouseEvent) { - if (this.contextmenu) { - os.contextMenu(this.contextmenu, ev); - } - }, +function onDblClick() { + maximize(); +} - // 最前面へ移動 - top() { - (this.$el as any).style.zIndex = os.claimZIndex(this.front ? 'middle' : 'low'); - }, +function onHeaderMousedown(evt: MouseEvent) { + // 右クリックはコンテキストメニューを開こうとした可能性が高いため無視 + if (evt.button === 2) return; - onBodyMousedown() { - this.top(); - }, + let beforeMaximized = false; - onHeaderMousedown(evt: MouseEvent) { - // 右クリックはコンテキストメニューを開こうとした可能性が高いため無視 - if (evt.button === 2) return; + if (maximized) { + beforeMaximized = true; + unMaximize(); + } - const main = this.$el as any; + // ダブルクリック判定 + if (Date.now() - beforeClickedAt < 300) { + beforeClickedAt = Date.now(); + onDblClick(); + return; + } - if (!contains(main, document.activeElement)) main.focus(); + beforeClickedAt = Date.now(); - const position = main.getBoundingClientRect(); + const main = rootEl; - const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX; - const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY; - const moveBaseX = clickX - position.left; - const moveBaseY = clickY - position.top; - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = main.offsetWidth; - const windowHeight = main.offsetHeight; + if (!contains(main, document.activeElement)) main.focus(); - // 動かした時 - dragListen(me => { - const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX; - const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY; + const position = main.getBoundingClientRect(); - let moveLeft = x - moveBaseX; - let moveTop = y - moveBaseY; + const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX; + const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY; + const moveBaseX = beforeMaximized ? parseInt(unMaximizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる + const moveBaseY = beforeMaximized ? 20 : clickY - position.top; + const browserWidth = window.innerWidth; + const browserHeight = window.innerHeight; + const windowWidth = main.offsetWidth; + const windowHeight = main.offsetHeight; - // 下はみ出し - if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight; + function move(x: number, y: number) { + let moveLeft = x - moveBaseX; + let moveTop = y - moveBaseY; - // 左はみ出し - if (moveLeft < 0) moveLeft = 0; + // 下はみ出し + if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight; - // 上はみ出し - if (moveTop < 0) moveTop = 0; + // 左はみ出し + if (moveLeft < 0) moveLeft = 0; - // 右はみ出し - if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth; + // 上はみ出し + if (moveTop < 0) moveTop = 0; - this.$el.style.left = moveLeft + 'px'; - this.$el.style.top = moveTop + 'px'; - }); - }, + // 右はみ出し + if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth; - // 上ハンドル掴み時 - onTopHandleMousedown(evt) { - const main = this.$el as any; + rootEl.style.left = moveLeft + 'px'; + rootEl.style.top = moveTop + 'px'; + } - const base = evt.clientY; - const height = parseInt(getComputedStyle(main, '').height, 10); - const top = parseInt(getComputedStyle(main, '').top, 10); + if (beforeMaximized) { + move(clickX, clickY); + } - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + move > 0) { - if (height + -move > minHeight) { - this.applyTransformHeight(height + -move); - this.applyTransformTop(top + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(minHeight); - this.applyTransformTop(top + (height - minHeight)); - } - } else { // 上のはみ出し時 - this.applyTransformHeight(top + height); - this.applyTransformTop(0); - } - }); - }, + // 動かした時 + dragListen(me => { + const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX; + const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY; - // 右ハンドル掴み時 - onRightHandleMousedown(evt) { - const main = this.$el as any; + move(x, y); + }); +} - const base = evt.clientX; - const width = parseInt(getComputedStyle(main, '').width, 10); - const left = parseInt(getComputedStyle(main, '').left, 10); - const browserWidth = window.innerWidth; +// 上ハンドル掴み時 +function onTopHandleMousedown(evt) { + const main = rootEl; - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + width + move < browserWidth) { - if (width + move > minWidth) { - this.applyTransformWidth(width + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(minWidth); - } - } else { // 右のはみ出し時 - this.applyTransformWidth(browserWidth - left); - } - }); - }, + const base = evt.clientY; + const height = parseInt(getComputedStyle(main, '').height, 10); + const top = parseInt(getComputedStyle(main, '').top, 10); - // 下ハンドル掴み時 - onBottomHandleMousedown(evt) { - const main = this.$el as any; + // 動かした時 + dragListen(me => { + const move = me.clientY - base; + if (top + move > 0) { + if (height + -move > minHeight) { + applyTransformHeight(height + -move); + applyTransformTop(top + move); + } else { // 最小の高さより小さくなろうとした時 + applyTransformHeight(minHeight); + applyTransformTop(top + (height - minHeight)); + } + } else { // 上のはみ出し時 + applyTransformHeight(top + height); + applyTransformTop(0); + } + }); +} - const base = evt.clientY; - const height = parseInt(getComputedStyle(main, '').height, 10); - const top = parseInt(getComputedStyle(main, '').top, 10); - const browserHeight = window.innerHeight; +// 右ハンドル掴み時 +function onRightHandleMousedown(evt) { + const main = rootEl; - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + height + move < browserHeight) { - if (height + move > minHeight) { - this.applyTransformHeight(height + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(minHeight); - } - } else { // 下のはみ出し時 - this.applyTransformHeight(browserHeight - top); - } - }); - }, + const base = evt.clientX; + const width = parseInt(getComputedStyle(main, '').width, 10); + const left = parseInt(getComputedStyle(main, '').left, 10); + const browserWidth = window.innerWidth; - // 左ハンドル掴み時 - onLeftHandleMousedown(evt) { - const main = this.$el as any; + // 動かした時 + dragListen(me => { + const move = me.clientX - base; + if (left + width + move < browserWidth) { + if (width + move > minWidth) { + applyTransformWidth(width + move); + } else { // 最小の幅より小さくなろうとした時 + applyTransformWidth(minWidth); + } + } else { // 右のはみ出し時 + applyTransformWidth(browserWidth - left); + } + }); +} - const base = evt.clientX; - const width = parseInt(getComputedStyle(main, '').width, 10); - const left = parseInt(getComputedStyle(main, '').left, 10); +// 下ハンドル掴み時 +function onBottomHandleMousedown(evt) { + const main = rootEl; - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + move > 0) { - if (width + -move > minWidth) { - this.applyTransformWidth(width + -move); - this.applyTransformLeft(left + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(minWidth); - this.applyTransformLeft(left + (width - minWidth)); - } - } else { // 左のはみ出し時 - this.applyTransformWidth(left + width); - this.applyTransformLeft(0); - } - }); - }, + const base = evt.clientY; + const height = parseInt(getComputedStyle(main, '').height, 10); + const top = parseInt(getComputedStyle(main, '').top, 10); + const browserHeight = window.innerHeight; + + // 動かした時 + dragListen(me => { + const move = me.clientY - base; + if (top + height + move < browserHeight) { + if (height + move > minHeight) { + applyTransformHeight(height + move); + } else { // 最小の高さより小さくなろうとした時 + applyTransformHeight(minHeight); + } + } else { // 下のはみ出し時 + applyTransformHeight(browserHeight - top); + } + }); +} + +// 左ハンドル掴み時 +function onLeftHandleMousedown(evt) { + const main = rootEl; + + const base = evt.clientX; + const width = parseInt(getComputedStyle(main, '').width, 10); + const left = parseInt(getComputedStyle(main, '').left, 10); + + // 動かした時 + dragListen(me => { + const move = me.clientX - base; + if (left + move > 0) { + if (width + -move > minWidth) { + applyTransformWidth(width + -move); + applyTransformLeft(left + move); + } else { // 最小の幅より小さくなろうとした時 + applyTransformWidth(minWidth); + applyTransformLeft(left + (width - minWidth)); + } + } else { // 左のはみ出し時 + applyTransformWidth(left + width); + applyTransformLeft(0); + } + }); +} + +// 左上ハンドル掴み時 +function onTopLeftHandleMousedown(evt) { + onTopHandleMousedown(evt); + onLeftHandleMousedown(evt); +} + +// 右上ハンドル掴み時 +function onTopRightHandleMousedown(evt) { + onTopHandleMousedown(evt); + onRightHandleMousedown(evt); +} + +// 右下ハンドル掴み時 +function onBottomRightHandleMousedown(evt) { + onBottomHandleMousedown(evt); + onRightHandleMousedown(evt); +} + +// 左下ハンドル掴み時 +function onBottomLeftHandleMousedown(evt) { + onBottomHandleMousedown(evt); + onLeftHandleMousedown(evt); +} + +// 高さを適用 +function applyTransformHeight(height) { + if (height > window.innerHeight) height = window.innerHeight; + rootEl.style.height = height + 'px'; +} + +// 幅を適用 +function applyTransformWidth(width) { + if (width > window.innerWidth) width = window.innerWidth; + rootEl.style.width = width + 'px'; +} - // 左上ハンドル掴み時 - onTopLeftHandleMousedown(evt) { - this.onTopHandleMousedown(evt); - this.onLeftHandleMousedown(evt); - }, +// Y座標を適用 +function applyTransformTop(top) { + rootEl.style.top = top + 'px'; +} - // 右上ハンドル掴み時 - onTopRightHandleMousedown(evt) { - this.onTopHandleMousedown(evt); - this.onRightHandleMousedown(evt); - }, +// X座標を適用 +function applyTransformLeft(left) { + rootEl.style.left = left + 'px'; +} - // 右下ハンドル掴み時 - onBottomRightHandleMousedown(evt) { - this.onBottomHandleMousedown(evt); - this.onRightHandleMousedown(evt); - }, +function onBrowserResize() { + const main = rootEl; + const position = main.getBoundingClientRect(); + const browserWidth = window.innerWidth; + const browserHeight = window.innerHeight; + const windowWidth = main.offsetWidth; + const windowHeight = main.offsetHeight; + if (position.left < 0) main.style.left = '0'; // 左はみ出し + if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し + if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し + if (position.top < 0) main.style.top = '0'; // 上はみ出し +} - // 左下ハンドル掴み時 - onBottomLeftHandleMousedown(evt) { - this.onBottomHandleMousedown(evt); - this.onLeftHandleMousedown(evt); - }, +onMounted(() => { + if (props.initialWidth) applyTransformWidth(props.initialWidth); + if (props.initialHeight) applyTransformHeight(props.initialHeight); - // 高さを適用 - applyTransformHeight(height) { - if (height > window.innerHeight) height = window.innerHeight; - (this.$el as any).style.height = height + 'px'; - }, + applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2)); + applyTransformLeft((window.innerWidth / 2) - (rootEl.offsetWidth / 2)); - // 幅を適用 - applyTransformWidth(width) { - if (width > window.innerWidth) width = window.innerWidth; - (this.$el as any).style.width = width + 'px'; - }, + // 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする + top(); - // Y座標を適用 - applyTransformTop(top) { - (this.$el as any).style.top = top + 'px'; - }, + window.addEventListener('resize', onBrowserResize); +}); - // X座標を適用 - applyTransformLeft(left) { - (this.$el as any).style.left = left + 'px'; - }, +onBeforeUnmount(() => { + window.removeEventListener('resize', onBrowserResize); +}); - onBrowserResize() { - const main = this.$el as any; - const position = main.getBoundingClientRect(); - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = main.offsetWidth; - const windowHeight = main.offsetHeight; - if (position.left < 0) main.style.left = 0; // 左はみ出し - if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し - if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し - if (position.top < 0) main.style.top = 0; // 上はみ出し - }, - }, +defineExpose({ + close, }); </script> @@ -384,7 +414,7 @@ export default defineComponent({ left: 0; > .body { - overflow: hidden; + overflow: clip; display: flex; flex-direction: column; contain: content; @@ -522,5 +552,11 @@ export default defineComponent({ cursor: nesw-resize; } } + + &.maximized { + > .body { + border-radius: 0; + } + } } </style> diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index f970660a4a..8b1cc6c124 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -330,13 +330,11 @@ definePageMetadata(INFO); width: 34%; padding-right: 32px; box-sizing: border-box; - overflow: auto; } > .main { flex: 1; min-width: 0; - overflow: auto; } } } diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index f30b0ccbdc..5bb3273b3f 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -1,11 +1,11 @@ <template> <div class="_formRoot"> <div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> - <div class="avatar _acrylic"> + <div class="avatar"> <MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/> - <MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> + <MkButton primary rounded class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> </div> - <MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> + <MkButton primary rounded class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> </div> <FormInput v-model="profile.name" :max="30" manual-save class="_formBlock"> @@ -187,6 +187,7 @@ definePageMetadata({ position: relative; background-size: cover; background-position: center; + border: solid 1px var(--divider); border-radius: 10px; overflow: clip; diff --git a/packages/client/src/scripts/popup-position.ts b/packages/client/src/scripts/popup-position.ts new file mode 100644 index 0000000000..e84eebf103 --- /dev/null +++ b/packages/client/src/scripts/popup-position.ts @@ -0,0 +1,158 @@ +import { Ref } from 'vue'; + +export function calcPopupPosition(el: HTMLElement, props: { + anchorElement: HTMLElement | null; + innerMargin: number; + direction: 'top' | 'bottom' | 'left' | 'right'; + align: 'top' | 'bottom' | 'left' | 'right' | 'center'; + alignOffset?: number; + x?: number; + y?: number; +}): { top: number; left: number; transformOrigin: string; } { + const contentWidth = el.offsetWidth; + const contentHeight = el.offsetHeight; + + let rect: DOMRect; + + if (props.anchorElement) { + rect = props.anchorElement.getBoundingClientRect(); + } + + const calcPosWhenTop = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; + } else { + left = props.x; + top = (props.y - contentHeight) - props.innerMargin; + } + + left -= (el.offsetWidth / 2); + + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + + return [left, top]; + }; + + const calcPosWhenBottom = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin; + } else { + left = props.x; + top = (props.y) + props.innerMargin; + } + + left -= (el.offsetWidth / 2); + + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + + return [left, top]; + }; + + const calcPosWhenLeft = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; + top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); + } else { + left = (props.x - contentWidth) - props.innerMargin; + top = props.y; + } + + top -= (el.offsetHeight / 2); + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } + + return [left, top]; + }; + + const calcPosWhenRight = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin; + + if (props.align === 'top') { + top = rect.top + window.pageYOffset; + if (props.alignOffset != null) top += props.alignOffset; + } else if (props.align === 'bottom') { + // TODO + } else { // center + top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); + top -= (el.offsetHeight / 2); + } + } else { + left = props.x + props.innerMargin; + top = props.y; + top -= (el.offsetHeight / 2); + } + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } + + return [left, top]; + }; + + const calc = (): { + left: number; + top: number; + transformOrigin: string; + } => { + switch (props.direction) { + case 'top': { + const [left, top] = calcPosWhenTop(); + + // ツールチップを上に向かって表示するスペースがなければ下に向かって出す + if (top - window.pageYOffset < 0) { + const [left, top] = calcPosWhenBottom(); + return { left, top, transformOrigin: 'center top' }; + } + + return { left, top, transformOrigin: 'center bottom' }; + } + + case 'bottom': { + const [left, top] = calcPosWhenBottom(); + // TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す + return { left, top, transformOrigin: 'center top' }; + } + + case 'left': { + const [left, top] = calcPosWhenLeft(); + + // ツールチップを左に向かって表示するスペースがなければ右に向かって出す + if (left - window.pageXOffset < 0) { + const [left, top] = calcPosWhenRight(); + return { left, top, transformOrigin: 'left center' }; + } + + return { left, top, transformOrigin: 'right center' }; + } + + case 'right': { + const [left, top] = calcPosWhenRight(); + // TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す + return { left, top, transformOrigin: 'left center' }; + } + } + }; + + return calc(); +} diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index 5033333313..3971214af0 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -270,7 +270,7 @@ type Plugin = { * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) */ import lightTheme from '@/themes/l-light.json5'; -import darkTheme from '@/themes/d-dark.json5'; +import darkTheme from '@/themes/d-green-lime.json5'; export class ColdDeviceStorage { public static default = { diff --git a/packages/client/src/types/menu.ts b/packages/client/src/types/menu.ts index ed67e6ab88..972f6db214 100644 --- a/packages/client/src/types/menu.ts +++ b/packages/client/src/types/menu.ts @@ -11,10 +11,11 @@ export type MenuA = { type: 'a', href: string, target?: string, download?: strin export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean }; export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; +export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] }; export type MenuPending = { type: 'pending' }; -type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; -type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton>; +type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent; +type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent>; export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; -export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; +export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent; diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue index 9f7388db53..f32cd3fe0d 100644 --- a/packages/client/src/ui/_common_/common.vue +++ b/packages/client/src/ui/_common_/common.vue @@ -1,5 +1,6 @@ <template> -<component :is="popup.component" +<component + :is="popup.component" v-for="popup in popups" :key="popup.id" v-bind="popup.props" @@ -15,56 +16,45 @@ <div v-if="dev" id="devTicker"><span>DEV BUILD</span></div> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent } from 'vue'; +import { swInject } from './sw-inject'; import { popup, popups, pendingApiRequestsCount } from '@/os'; import { uploads } from '@/scripts/upload'; import * as sound from '@/scripts/sound'; import { $i } from '@/account'; -import { swInject } from './sw-inject'; import { stream } from '@/stream'; -export default defineComponent({ - components: { - XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')), - XUpload: defineAsyncComponent(() => import('./upload.vue')), - }, +const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue')); +const XUpload = defineAsyncComponent(() => import('./upload.vue')); - setup() { - const onNotification = notification => { - if ($i.mutingNotificationTypes.includes(notification.type)) return; +const dev = _DEV_; - if (document.visibilityState === 'visible') { - stream.send('readNotification', { - id: notification.id - }); +const onNotification = notification => { + if ($i.mutingNotificationTypes.includes(notification.type)) return; - popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), { - notification - }, {}, 'closed'); - } + if (document.visibilityState === 'visible') { + stream.send('readNotification', { + id: notification.id, + }); - sound.play('notification'); - }; + popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), { + notification, + }, {}, 'closed'); + } - if ($i) { - const connection = stream.useChannel('main', null, 'UI'); - connection.on('notification', onNotification); + sound.play('notification'); +}; - //#region Listen message from SW - if ('serviceWorker' in navigator) { - swInject(); - } - } +if ($i) { + const connection = stream.useChannel('main', null, 'UI'); + connection.on('notification', onNotification); - return { - uploads, - popups, - pendingApiRequestsCount, - dev: _DEV_, - }; - }, -}); + //#region Listen message from SW + if ('serviceWorker' in navigator) { + swInject(); + } +} </script> <style lang="scss"> diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue index d1b4c30b31..f2521cfc72 100644 --- a/packages/client/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue @@ -87,6 +87,36 @@ function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.federation, icon: 'fas fa-globe', to: '/about#federation', + }, null, { + type: 'parent', + text: i18n.ts.help, + icon: 'fas fa-question-circle', + children: [{ + type: 'link', + to: '/mfm-cheat-sheet', + text: i18n.ts._mfm.cheatSheet, + icon: 'fas fa-code', + }, { + type: 'link', + to: '/scratchpad', + text: i18n.ts.scratchpad, + icon: 'fas fa-terminal', + }, { + type: 'link', + to: '/api-console', + text: 'API Console', + icon: 'fas fa-terminal', + }, null, { + text: i18n.ts.document, + icon: 'fas fa-question-circle', + action: () => { + window.open('https://misskey-hub.net/help.html', '_blank'); + }, + }], + }, { + type: 'link', + text: i18n.ts.aboutMisskey, + to: '/about-misskey', }], ev.currentTarget ?? ev.target, { align: 'left', }); diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue index e18f89113f..7e6065c305 100644 --- a/packages/client/src/ui/_common_/navbar.vue +++ b/packages/client/src/ui/_common_/navbar.vue @@ -110,6 +110,36 @@ function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.federation, icon: 'fas fa-globe', to: '/about#federation', + }, null, { + type: 'parent', + text: i18n.ts.help, + icon: 'fas fa-question-circle', + children: [{ + type: 'link', + to: '/mfm-cheat-sheet', + text: i18n.ts._mfm.cheatSheet, + icon: 'fas fa-code', + }, { + type: 'link', + to: '/scratchpad', + text: i18n.ts.scratchpad, + icon: 'fas fa-terminal', + }, { + type: 'link', + to: '/api-console', + text: 'API Console', + icon: 'fas fa-terminal', + }, null, { + text: i18n.ts.document, + icon: 'fas fa-question-circle', + action: () => { + window.open('https://misskey-hub.net/help.html', '_blank'); + }, + }], + }, { + type: 'link', + text: i18n.ts.aboutMisskey, + to: '/about-misskey', }], ev.currentTarget ?? ev.target, { align: 'left', }); diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue index 1a1bd7d266..7b30ffad45 100644 --- a/packages/client/src/ui/deck/column.vue +++ b/packages/client/src/ui/deck/column.vue @@ -128,31 +128,36 @@ function getMenu() { if (canceled) return; updateColumn(props.column.id, result); }, - }, null, { - icon: 'fas fa-arrow-left', - text: i18n.ts._deck.swapLeft, - action: () => { - swapLeftColumn(props.column.id); - }, }, { - icon: 'fas fa-arrow-right', - text: i18n.ts._deck.swapRight, - action: () => { - swapRightColumn(props.column.id); - }, - }, props.isStacked ? { - icon: 'fas fa-arrow-up', - text: i18n.ts._deck.swapUp, - action: () => { - swapUpColumn(props.column.id); - }, - } : undefined, props.isStacked ? { - icon: 'fas fa-arrow-down', - text: i18n.ts._deck.swapDown, - action: () => { - swapDownColumn(props.column.id); - }, - } : undefined, null, { + type: 'parent', + text: i18n.ts.move + '...', + icon: 'fas fa-arrows-up-down-left-right', + children: [{ + icon: 'fas fa-arrow-left', + text: i18n.ts._deck.swapLeft, + action: () => { + swapLeftColumn(props.column.id); + }, + }, { + icon: 'fas fa-arrow-right', + text: i18n.ts._deck.swapRight, + action: () => { + swapRightColumn(props.column.id); + }, + }, props.isStacked ? { + icon: 'fas fa-arrow-up', + text: i18n.ts._deck.swapUp, + action: () => { + swapUpColumn(props.column.id); + }, + } : undefined, props.isStacked ? { + icon: 'fas fa-arrow-down', + text: i18n.ts._deck.swapDown, + action: () => { + swapDownColumn(props.column.id); + }, + } : undefined], + }, { icon: 'fas fa-window-restore', text: i18n.ts._deck.stackLeft, action: () => { |