diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-06-20 17:38:49 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-20 17:38:49 +0900 |
| commit | 699f24f3dcdb156838eb70602885c0b2cdd02cbc (patch) | |
| tree | 45b28eeadbb7d9e7f3847bd04f75ed010153619a /packages/client/src/components | |
| parent | refactor: チャットルームをComposition API化 (#8850) (diff) | |
| download | sharkey-699f24f3dcdb156838eb70602885c0b2cdd02cbc.tar.gz sharkey-699f24f3dcdb156838eb70602885c0b2cdd02cbc.tar.bz2 sharkey-699f24f3dcdb156838eb70602885c0b2cdd02cbc.zip | |
refactor(client): Refine routing (#8846)
Diffstat (limited to 'packages/client/src/components')
| -rw-r--r-- | packages/client/src/components/chart.vue | 94 | ||||
| -rw-r--r-- | packages/client/src/components/drive-file-thumbnail.vue | 2 | ||||
| -rw-r--r-- | packages/client/src/components/form/folder.vue | 4 | ||||
| -rw-r--r-- | packages/client/src/components/global/a.vue | 31 | ||||
| -rw-r--r-- | packages/client/src/components/global/header.vue | 361 | ||||
| -rw-r--r-- | packages/client/src/components/global/page-header.vue | 300 | ||||
| -rw-r--r-- | packages/client/src/components/global/router-view.vue | 39 | ||||
| -rw-r--r-- | packages/client/src/components/index.ts | 9 | ||||
| -rw-r--r-- | packages/client/src/components/modal-page-window.vue | 207 | ||||
| -rw-r--r-- | packages/client/src/components/note.vue | 2 | ||||
| -rw-r--r-- | packages/client/src/components/page-window.vue | 245 | ||||
| -rw-r--r-- | packages/client/src/components/ui/window.vue | 69 |
12 files changed, 632 insertions, 731 deletions
diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index 4e9c4e587a..5e9c2f03be 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -13,7 +13,7 @@ id-denylist violation when setting it. This is causing about 60+ lint issues. As this is part of Chart.js's API it makes sense to disable the check here. */ -import { defineProps, onMounted, ref, watch, PropType, onUnmounted } from 'vue'; +import { onMounted, ref, watch, PropType, onUnmounted } from 'vue'; import { Chart, ArcElement, @@ -53,7 +53,7 @@ const props = defineProps({ limit: { type: Number, required: false, - default: 90 + default: 90, }, span: { type: String as PropType<'hour' | 'day'>, @@ -62,22 +62,22 @@ const props = defineProps({ detailed: { type: Boolean, required: false, - default: false + default: false, }, stacked: { type: Boolean, required: false, - default: false + default: false, }, bar: { type: Boolean, required: false, - default: false + default: false, }, aspectRatio: { type: Number, required: false, - default: null + default: null, }, }); @@ -156,7 +156,7 @@ const getDate = (ago: number) => { const format = (arr) => { return arr.map((v, i) => ({ x: getDate(i).getTime(), - y: v + y: v, })); }; @@ -343,7 +343,7 @@ const render = () => { min: 'original', max: 'original', }, - } + }, } : undefined, //gradient, }, @@ -367,8 +367,8 @@ const render = () => { ctx.stroke(); ctx.restore(); } - } - }] + }, + }], }); }; @@ -433,18 +433,18 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => { name: 'In', type: 'area', color: '#008FFB', - data: format(raw.inboxReceived) + data: format(raw.inboxReceived), }, { name: 'Out (succ)', type: 'area', color: '#00E396', - data: format(raw.deliverSucceeded) + data: format(raw.deliverSucceeded), }, { name: 'Out (fail)', type: 'area', color: '#FEB019', - data: format(raw.deliverFailed) - }] + data: format(raw.deliverFailed), + }], }; }; @@ -456,7 +456,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { type: 'line', data: format(type === 'combined' ? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) - : sum(raw[type].inc, negate(raw[type].dec)) + : sum(raw[type].inc, negate(raw[type].dec)), ), color: '#888888', }, { @@ -464,7 +464,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { type: 'area', data: format(type === 'combined' ? sum(raw.local.diffs.renote, raw.remote.diffs.renote) - : raw[type].diffs.renote + : raw[type].diffs.renote, ), color: colors.green, }, { @@ -472,7 +472,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { type: 'area', data: format(type === 'combined' ? sum(raw.local.diffs.reply, raw.remote.diffs.reply) - : raw[type].diffs.reply + : raw[type].diffs.reply, ), color: colors.yellow, }, { @@ -480,7 +480,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { type: 'area', data: format(type === 'combined' ? sum(raw.local.diffs.normal, raw.remote.diffs.normal) - : raw[type].diffs.normal + : raw[type].diffs.normal, ), color: colors.blue, }, { @@ -488,7 +488,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { type: 'area', data: format(type === 'combined' ? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile) - : raw[type].diffs.withFile + : raw[type].diffs.withFile, ), color: colors.purple, }], @@ -522,21 +522,21 @@ const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => { type: 'line', data: format(total ? sum(raw.local.total, raw.remote.total) - : sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) + : sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)), ), }, { name: 'Local', type: 'area', data: format(total ? raw.local.total - : sum(raw.local.inc, negate(raw.local.dec)) + : sum(raw.local.inc, negate(raw.local.dec)), ), }, { name: 'Remote', type: 'area', data: format(total ? raw.remote.total - : sum(raw.remote.inc, negate(raw.remote.dec)) + : sum(raw.remote.inc, negate(raw.remote.dec)), ), }], }; @@ -607,8 +607,8 @@ const fetchDriveChart = async (): Promise<typeof chartData> => { raw.local.incSize, negate(raw.local.decSize), raw.remote.incSize, - negate(raw.remote.decSize) - ) + negate(raw.remote.decSize), + ), ), }, { name: 'Local +', @@ -642,8 +642,8 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => { raw.local.incCount, negate(raw.local.decCount), raw.remote.incCount, - negate(raw.remote.decCount) - ) + negate(raw.remote.decCount), + ), ), }, { name: 'Local +', @@ -672,18 +672,18 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => { name: 'In', type: 'area', color: '#008FFB', - data: format(raw.requests.received) + data: format(raw.requests.received), }, { name: 'Out (succ)', type: 'area', color: '#00E396', - data: format(raw.requests.succeeded) + data: format(raw.requests.succeeded), }, { name: 'Out (fail)', type: 'area', color: '#FEB019', - data: format(raw.requests.failed) - }] + data: format(raw.requests.failed), + }], }; }; @@ -696,9 +696,9 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData color: '#008FFB', data: format(total ? raw.users.total - : sum(raw.users.inc, negate(raw.users.dec)) - ) - }] + : sum(raw.users.inc, negate(raw.users.dec)), + ), + }], }; }; @@ -711,9 +711,9 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData color: '#008FFB', data: format(total ? raw.notes.total - : sum(raw.notes.inc, negate(raw.notes.dec)) - ) - }] + : sum(raw.notes.inc, negate(raw.notes.dec)), + ), + }], }; }; @@ -726,17 +726,17 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> = color: '#008FFB', data: format(total ? raw.following.total - : sum(raw.following.inc, negate(raw.following.dec)) - ) + : sum(raw.following.inc, negate(raw.following.dec)), + ), }, { name: 'Followers', type: 'area', color: '#00E396', data: format(total ? raw.followers.total - : sum(raw.followers.inc, negate(raw.followers.dec)) - ) - }] + : sum(raw.followers.inc, negate(raw.followers.dec)), + ), + }], }; }; @@ -750,9 +750,9 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char color: '#008FFB', data: format(total ? raw.drive.totalUsage - : sum(raw.drive.incUsage, negate(raw.drive.decUsage)) - ) - }] + : sum(raw.drive.incUsage, negate(raw.drive.decUsage)), + ), + }], }; }; @@ -765,9 +765,9 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char color: '#008FFB', data: format(total ? raw.drive.totalFiles - : sum(raw.drive.incFiles, negate(raw.drive.decFiles)) - ) - }] + : sum(raw.drive.incFiles, negate(raw.drive.decFiles)), + ), + }], }; }; diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue index 07cd565c58..b346585cec 100644 --- a/packages/client/src/components/drive-file-thumbnail.vue +++ b/packages/client/src/components/drive-file-thumbnail.vue @@ -57,7 +57,7 @@ const isThumbnailAvailable = computed(() => { .zdjebgpv { position: relative; display: flex; - background: #e1e1e1; + background: var(--panel); border-radius: 8px; overflow: clip; diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue index 1b960657d7..a9d8bd97b8 100644 --- a/packages/client/src/components/form/folder.vue +++ b/packages/client/src/components/form/folder.vue @@ -9,13 +9,13 @@ <i v-else class="fas fa-angle-down icon"></i> </span> </div> - <keep-alive> + <KeepAlive> <div v-if="openedAtLeastOnce" v-show="opened" class="body"> <MkSpacer :margin-min="14" :margin-max="22"> <slot></slot> </MkSpacer> </div> - </keep-alive> + </KeepAlive> </div> </template> diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue index 5287d59b3e..c7cf12e8c8 100644 --- a/packages/client/src/components/global/a.vue +++ b/packages/client/src/components/global/a.vue @@ -5,13 +5,13 @@ </template> <script lang="ts" setup> +import { inject } from 'vue'; import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { router } from '@/router'; import { url } from '@/config'; import { popout as popout_ } from '@/scripts/popout'; import { i18n } from '@/i18n'; -import { MisskeyNavigator } from '@/scripts/navigate'; +import { useRouter } from '@/router'; const props = withDefaults(defineProps<{ to: string; @@ -22,15 +22,16 @@ const props = withDefaults(defineProps<{ behavior: null, }); -const mkNav = new MisskeyNavigator(); +const router = useRouter(); const active = $computed(() => { if (props.activeClass == null) return false; const resolved = router.resolve(props.to); - if (resolved.path === router.currentRoute.value.path) return true; - if (resolved.name == null) return false; + if (resolved == null) return false; + if (resolved.route.path === router.currentRoute.value.path) return true; + if (resolved.route.name == null) return false; if (router.currentRoute.value.name == null) return false; - return resolved.name === router.currentRoute.value.name; + return resolved.route.name === router.currentRoute.value.name; }); function onContextmenu(ev) { @@ -44,31 +45,25 @@ function onContextmenu(ev) { text: i18n.ts.openInWindow, action: () => { os.pageWindow(props.to); - } - }, mkNav.sideViewHook ? { - icon: 'fas fa-columns', - text: i18n.ts.openInSideView, - action: () => { - if (mkNav.sideViewHook) mkNav.sideViewHook(props.to); - } - } : undefined, { + }, + }, { icon: 'fas fa-expand-alt', text: i18n.ts.showInPage, action: () => { router.push(props.to); - } + }, }, null, { icon: 'fas fa-external-link-alt', text: i18n.ts.openInNewTab, action: () => { window.open(props.to, '_blank'); - } + }, }, { icon: 'fas fa-link', text: i18n.ts.copyLink, action: () => { copyToClipboard(`${url}${props.to}`); - } + }, }], ev); } @@ -98,6 +93,6 @@ function nav() { } } - mkNav.push(props.to); + router.push(props.to); } </script> diff --git a/packages/client/src/components/global/header.vue b/packages/client/src/components/global/header.vue deleted file mode 100644 index 63db19a520..0000000000 --- a/packages/client/src/components/global/header.vue +++ /dev/null @@ -1,361 +0,0 @@ -<template> -<div ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick"> - <template v-if="info"> - <div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup"> - <MkAvatar v-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/> - <i v-else-if="info.icon" class="icon" :class="info.icon"></i> - - <div class="title"> - <MkUserName v-if="info.userName" :user="info.userName" :nowrap="true" class="title"/> - <div v-else-if="info.title" class="title">{{ info.title }}</div> - <div v-if="!narrow && info.subtitle" class="subtitle"> - {{ info.subtitle }} - </div> - <div v-if="narrow && hasTabs" class="subtitle activeTab"> - {{ info.tabs.find(tab => tab.active)?.title }} - <i class="chevron fas fa-chevron-down"></i> - </div> - </div> - </div> - <div v-if="!narrow || hideTitle" class="tabs"> - <button v-for="tab in info.tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick"> - <i v-if="tab.icon" class="icon" :class="tab.icon"></i> - <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> - </button> - </div> - </template> - <div class="buttons right"> - <template v-if="info && info.actions && !narrow"> - <template v-for="action in info.actions"> - <MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton> - <button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> - </template> - </template> - <button v-if="shouldShowMenu" v-tooltip="$ts.menu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag"><i class="fas fa-ellipsis-h"></i></button> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue'; -import tinycolor from 'tinycolor2'; -import { popupMenu } from '@/os'; -import { url } from '@/config'; -import { scrollToTop } from '@/scripts/scroll'; -import MkButton from '@/components/ui/button.vue'; -import { i18n } from '@/i18n'; -import { globalEvents } from '@/events'; - -export default defineComponent({ - components: { - MkButton - }, - - props: { - info: { - type: Object as PropType<{ - actions?: {}[]; - tabs?: {}[]; - }>, - required: true - }, - menu: { - required: false - }, - thin: { - required: false, - default: false - }, - }, - - setup(props) { - const el = ref<HTMLElement>(null); - const bg = ref(null); - const narrow = ref(false); - const height = ref(0); - const hasTabs = computed(() => { - return props.info.tabs && props.info.tabs.length > 0; - }); - const shouldShowMenu = computed(() => { - if (props.info == null) return false; - if (props.info.actions != null && narrow.value) return true; - if (props.info.menu != null) return true; - if (props.info.share != null) return true; - if (props.menu != null) return true; - return false; - }); - - const share = () => { - navigator.share({ - url: url + props.info.path, - ...props.info.share, - }); - }; - - const showMenu = (ev: MouseEvent) => { - let menu = props.info.menu ? props.info.menu() : []; - if (narrow.value && props.info.actions) { - menu = [...props.info.actions.map(x => ({ - text: x.text, - icon: x.icon, - action: x.handler - })), menu.length > 0 ? null : undefined, ...menu]; - } - if (props.info.share) { - if (menu.length > 0) menu.push(null); - menu.push({ - text: i18n.ts.share, - icon: 'fas fa-share-alt', - action: share - }); - } - if (props.menu) { - if (menu.length > 0) menu.push(null); - menu = menu.concat(props.menu); - } - popupMenu(menu, ev.currentTarget ?? ev.target); - }; - - const showTabsPopup = (ev: MouseEvent) => { - if (!hasTabs.value) return; - if (!narrow.value) return; - ev.preventDefault(); - ev.stopPropagation(); - const menu = props.info.tabs.map(tab => ({ - text: tab.title, - icon: tab.icon, - action: tab.onClick, - })); - popupMenu(menu, ev.currentTarget ?? ev.target); - }; - - const preventDrag = (ev: TouchEvent) => { - ev.stopPropagation(); - }; - - const onClick = () => { - scrollToTop(el.value, { behavior: 'smooth' }); - }; - - const calcBg = () => { - const rawBg = props.info?.bg || 'var(--bg)'; - const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); - tinyBg.setAlpha(0.85); - bg.value = tinyBg.toRgbString(); - }; - - onMounted(() => { - calcBg(); - globalEvents.on('themeChanged', calcBg); - onUnmounted(() => { - globalEvents.off('themeChanged', calcBg); - }); - - if (el.value.parentElement) { - narrow.value = el.value.parentElement.offsetWidth < 500; - const ro = new ResizeObserver((entries, observer) => { - if (el.value) { - narrow.value = el.value.parentElement.offsetWidth < 500; - } - }); - ro.observe(el.value.parentElement); - onUnmounted(() => { - ro.disconnect(); - }); - } - }); - - return { - el, - bg, - narrow, - height, - hasTabs, - shouldShowMenu, - share, - showMenu, - showTabsPopup, - preventDrag, - onClick, - hideTitle: inject('shouldOmitHeaderTitle', false), - thin_: props.thin || inject('shouldHeaderThin', false) - }; - }, -}); -</script> - -<style lang="scss" scoped> -.fdidabkb { - --height: 60px; - display: flex; - position: sticky; - top: var(--stickyTop, 0); - z-index: 1000; - width: 100%; - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - border-bottom: solid 0.5px var(--divider); - - &.thin { - --height: 50px; - - > .buttons { - > .button { - font-size: 0.9em; - } - } - } - - &.slim { - text-align: center; - - > .titleContainer { - flex: 1; - margin: 0 auto; - margin-left: var(--height); - - > *:first-child { - margin-left: auto; - } - - > *:last-child { - margin-right: auto; - } - } - } - - > .buttons { - --margin: 8px; - display: flex; - align-items: center; - height: var(--height); - margin: 0 var(--margin); - - &.right { - margin-left: auto; - } - - &:empty { - width: var(--height); - } - - > .button { - display: flex; - align-items: center; - justify-content: center; - height: calc(var(--height) - (var(--margin) * 2)); - width: calc(var(--height) - (var(--margin) * 2)); - box-sizing: border-box; - position: relative; - border-radius: 5px; - - &:hover { - background: rgba(0, 0, 0, 0.05); - } - - &.highlighted { - color: var(--accent); - } - } - - > .fullButton { - & + .fullButton { - margin-left: 12px; - } - } - } - - > .titleContainer { - display: flex; - align-items: center; - max-width: 400px; - overflow: auto; - white-space: nowrap; - text-align: left; - font-weight: bold; - flex-shrink: 0; - margin-left: 24px; - - > .avatar { - $size: 32px; - display: inline-block; - width: $size; - height: $size; - vertical-align: bottom; - margin: 0 8px; - pointer-events: none; - } - - > .icon { - margin-right: 8px; - } - - > .title { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - line-height: 1.1; - - > .subtitle { - opacity: 0.6; - font-size: 0.8em; - font-weight: normal; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &.activeTab { - text-align: center; - - > .chevron { - display: inline-block; - margin-left: 6px; - } - } - } - } - } - - > .tabs { - margin-left: 16px; - font-size: 0.8em; - overflow: auto; - white-space: nowrap; - - > .tab { - display: inline-block; - position: relative; - padding: 0 10px; - height: 100%; - font-weight: normal; - opacity: 0.7; - - &:hover { - opacity: 1; - } - - &.active { - opacity: 1; - - &:after { - content: ""; - display: block; - position: absolute; - bottom: 0; - left: 0; - right: 0; - margin: 0 auto; - width: 100%; - height: 3px; - background: var(--accent); - } - } - - > .icon + .title { - margin-left: 8px; - } - } - } -} -</style> diff --git a/packages/client/src/components/global/page-header.vue b/packages/client/src/components/global/page-header.vue new file mode 100644 index 0000000000..c01631c6a3 --- /dev/null +++ b/packages/client/src/components/global/page-header.vue @@ -0,0 +1,300 @@ +<template> +<div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick"> + <template v-if="metadata"> + <div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup"> + <MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/> + <i v-else-if="metadata.icon" class="icon" :class="metadata.icon"></i> + + <div class="title"> + <MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/> + <div v-else-if="metadata.title" class="title">{{ metadata.title }}</div> + <div v-if="!narrow && metadata.subtitle" class="subtitle"> + {{ metadata.subtitle }} + </div> + <div v-if="narrow && hasTabs" class="subtitle activeTab"> + {{ tabs.find(tab => tab.active)?.title }} + <i class="chevron fas fa-chevron-down"></i> + </div> + </div> + </div> + <div v-if="!narrow || hideTitle" class="tabs"> + <button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick"> + <i v-if="tab.icon" class="icon" :class="tab.icon"></i> + <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> + </button> + </div> + </template> + <div class="buttons right"> + <template v-for="action in actions"> + <button v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> + </template> + </div> +</div> +</template> + +<script lang="ts" setup> +import { computed, onMounted, onUnmounted, ref, inject } from 'vue'; +import tinycolor from 'tinycolor2'; +import { popupMenu } from '@/os'; +import { scrollToTop } from '@/scripts/scroll'; +import { i18n } from '@/i18n'; +import { globalEvents } from '@/events'; +import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata'; + +const props = defineProps<{ + tabs?: { + title: string; + active: boolean; + icon?: string; + iconOnly?: boolean; + onClick: () => void; + }[]; + actions?: { + text: string; + icon: string; + handler: (ev: MouseEvent) => void; + }[]; + thin?: boolean; +}>(); + +const metadata = injectPageMetadata(); + +const hideTitle = inject('shouldOmitHeaderTitle', false); +const thin_ = props.thin || inject('shouldHeaderThin', false); + +const el = $ref<HTMLElement | null>(null); +const bg = ref(null); +let narrow = $ref(false); +const height = ref(0); +const hasTabs = $computed(() => props.tabs && props.tabs.length > 0); +const hasActions = $computed(() => props.actions && props.actions.length > 0); +const show = $computed(() => { + return !hideTitle || hasTabs || hasActions; +}); + +const showTabsPopup = (ev: MouseEvent) => { + if (!hasTabs) return; + if (!narrow) return; + ev.preventDefault(); + ev.stopPropagation(); + const menu = props.tabs.map(tab => ({ + text: tab.title, + icon: tab.icon, + action: tab.onClick, + })); + popupMenu(menu, ev.currentTarget ?? ev.target); +}; + +const preventDrag = (ev: TouchEvent) => { + ev.stopPropagation(); +}; + +const onClick = () => { + scrollToTop(el, { behavior: 'smooth' }); +}; + +const calcBg = () => { + const rawBg = metadata?.bg || 'var(--bg)'; + const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); + tinyBg.setAlpha(0.85); + bg.value = tinyBg.toRgbString(); +}; + +let ro: ResizeObserver | null; + +onMounted(() => { + calcBg(); + globalEvents.on('themeChanged', calcBg); + + if (el && el.parentElement) { + narrow = el.parentElement.offsetWidth < 500; + ro = new ResizeObserver((entries, observer) => { + if (el.parentElement) { + narrow = el.parentElement.offsetWidth < 500; + } + }); + ro.observe(el.parentElement); + } +}); + +onUnmounted(() => { + globalEvents.off('themeChanged', calcBg); + if (ro) ro.disconnect(); +}); +</script> + +<style lang="scss" scoped> +.fdidabkb { + --height: 60px; + display: flex; + position: sticky; + top: var(--stickyTop, 0); + z-index: 1000; + width: 100%; + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); + border-bottom: solid 0.5px var(--divider); + + &.thin { + --height: 50px; + + > .buttons { + > .button { + font-size: 0.9em; + } + } + } + + &.slim { + text-align: center; + + > .titleContainer { + flex: 1; + margin: 0 auto; + margin-left: var(--height); + + > *:first-child { + margin-left: auto; + } + + > *:last-child { + margin-right: auto; + } + } + } + + > .buttons { + --margin: 8px; + display: flex; + align-items: center; + height: var(--height); + margin: 0 var(--margin); + + &.right { + margin-left: auto; + } + + &:empty { + width: var(--height); + } + + > .button { + display: flex; + align-items: center; + justify-content: center; + height: calc(var(--height) - (var(--margin) * 2)); + width: calc(var(--height) - (var(--margin) * 2)); + box-sizing: border-box; + position: relative; + border-radius: 5px; + + &:hover { + background: rgba(0, 0, 0, 0.05); + } + + &.highlighted { + color: var(--accent); + } + } + + > .fullButton { + & + .fullButton { + margin-left: 12px; + } + } + } + + > .titleContainer { + display: flex; + align-items: center; + max-width: 400px; + overflow: auto; + white-space: nowrap; + text-align: left; + font-weight: bold; + flex-shrink: 0; + margin-left: 24px; + + > .avatar { + $size: 32px; + display: inline-block; + width: $size; + height: $size; + vertical-align: bottom; + margin: 0 8px; + pointer-events: none; + } + + > .icon { + margin-right: 8px; + } + + > .title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.1; + + > .subtitle { + opacity: 0.6; + font-size: 0.8em; + font-weight: normal; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &.activeTab { + text-align: center; + + > .chevron { + display: inline-block; + margin-left: 6px; + } + } + } + } + } + + > .tabs { + margin-left: 16px; + font-size: 0.8em; + overflow: auto; + white-space: nowrap; + + > .tab { + display: inline-block; + position: relative; + padding: 0 10px; + height: 100%; + font-weight: normal; + opacity: 0.7; + + &:hover { + opacity: 1; + } + + &.active { + opacity: 1; + + &:after { + content: ""; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + margin: 0 auto; + width: 100%; + height: 3px; + background: var(--accent); + } + } + + > .icon + .title { + margin-left: 8px; + } + } + } +} +</style> diff --git a/packages/client/src/components/global/router-view.vue b/packages/client/src/components/global/router-view.vue new file mode 100644 index 0000000000..393ba30c3d --- /dev/null +++ b/packages/client/src/components/global/router-view.vue @@ -0,0 +1,39 @@ +<template> +<KeepAlive max="5"> + <component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> +</KeepAlive> +</template> + +<script lang="ts" setup> +import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue'; +import { Router } from '@/nirax'; + +const props = defineProps<{ + router?: Router; +}>(); + +const emit = defineEmits<{ +}>(); + +const router = props.router ?? inject('router'); + +if (router == null) { + throw new Error('no router provided'); +} + +let currentPageComponent = $ref(router.getCurrentComponent()); +let currentPageProps = $ref(router.getCurrentProps()); +let key = $ref(router.getCurrentKey()); + +function onChange({ route, props: newProps, key: newKey }) { + currentPageComponent = route.component; + currentPageProps = newProps; + key = newKey; +} + +router.addListener('change', onChange); + +onUnmounted(() => { + router.removeListener('change', onChange); +}); +</script> diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts index 26bac63245..aa8a591e51 100644 --- a/packages/client/src/components/index.ts +++ b/packages/client/src/components/index.ts @@ -10,15 +10,17 @@ import MkEllipsis from './global/ellipsis.vue'; import MkTime from './global/time.vue'; import MkUrl from './global/url.vue'; import I18n from './global/i18n'; +import RouterView from './global/router-view.vue'; import MkLoading from './global/loading.vue'; import MkError from './global/error.vue'; import MkAd from './global/ad.vue'; -import MkHeader from './global/header.vue'; +import MkPageHeader from './global/page-header.vue'; import MkSpacer from './global/spacer.vue'; import MkStickyContainer from './global/sticky-container.vue'; export default function(app: App) { app.component('I18n', I18n); + app.component('RouterView', RouterView); app.component('Mfm', Mfm); app.component('MkA', MkA); app.component('MkAcct', MkAcct); @@ -31,7 +33,7 @@ export default function(app: App) { app.component('MkLoading', MkLoading); app.component('MkError', MkError); app.component('MkAd', MkAd); - app.component('MkHeader', MkHeader); + app.component('MkPageHeader', MkPageHeader); app.component('MkSpacer', MkSpacer); app.component('MkStickyContainer', MkStickyContainer); } @@ -39,6 +41,7 @@ export default function(app: App) { declare module '@vue/runtime-core' { export interface GlobalComponents { I18n: typeof I18n; + RouterView: typeof RouterView; Mfm: typeof Mfm; MkA: typeof MkA; MkAcct: typeof MkAcct; @@ -51,7 +54,7 @@ declare module '@vue/runtime-core' { MkLoading: typeof MkLoading; MkError: typeof MkError; MkAd: typeof MkAd; - MkHeader: typeof MkHeader; + MkPageHeader: typeof MkPageHeader; MkSpacer: typeof MkSpacer; MkStickyContainer: typeof MkStickyContainer; } diff --git a/packages/client/src/components/modal-page-window.vue b/packages/client/src/components/modal-page-window.vue index 21bdb657b7..aef70f113b 100644 --- a/packages/client/src/components/modal-page-window.vue +++ b/packages/client/src/components/modal-page-window.vue @@ -1,163 +1,118 @@ <template> <MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> - <div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }"> + <div ref="rootEl" class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }"> <div class="header" @contextmenu="onContextmenu"> <button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button> <span v-else style="display: inline-block; width: 20px"></span> - <span v-if="pageInfo" class="title"> - <i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i> - <span>{{ pageInfo.title }}</span> + <span v-if="pageMetadata?.value" class="title"> + <i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i> + <span>{{ pageMetadata?.value.title }}</span> </span> <button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button> </div> <div class="body"> <MkStickyContainer> - <template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template> - <keep-alive> - <component :is="component" v-bind="props" :ref="changePage"/> - </keep-alive> + <template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template> + <RouterView :router="router"/> </MkStickyContainer> </div> </div> </MkModal> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ComputedRef, provide } from 'vue'; import MkModal from '@/components/ui/modal.vue'; -import { popout } from '@/scripts/popout'; +import { popout as _popout } from '@/scripts/popout'; import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { resolve } from '@/router'; import { url } from '@/config'; -import * as symbols from '@/symbols'; import * as os from '@/os'; +import { mainRouter, routes } from '@/router'; +import { i18n } from '@/i18n'; +import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; +import { Router } from '@/nirax'; -export default defineComponent({ - components: { - MkModal, - }, +const props = defineProps<{ + initialPath: string; +}>(); - inject: { - sideViewHook: { - default: null, - }, - }, - - provide() { - return { - navHook: (path) => { - this.navigate(path); - }, - shouldHeaderThin: true, - }; - }, +defineEmits<{ + (ev: 'closed'): void; + (ev: 'click'): void; +}>(); - props: { - initialPath: { - type: String, - required: true, - }, - initialComponent: { - type: Object, - required: true, - }, - initialProps: { - type: Object, - required: false, - default: () => {}, - }, - }, +const router = new Router(routes, props.initialPath); - emits: ['closed'], +router.addListener('push', ctx => { + +}); - data() { - return { - width: 860, - height: 660, - pageInfo: null, - path: this.initialPath, - component: this.initialComponent, - props: this.initialProps, - history: [], - }; - }, +let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); +let rootEl = $ref(); +let modal = $ref<InstanceType<typeof MkModal>>(); +let path = $ref(props.initialPath); +let width = $ref(860); +let height = $ref(660); +const history = []; - computed: { - url(): string { - return url + this.path; - }, +provide('router', router); +provideMetadataReceiver((info) => { + pageMetadata = info; +}); +provide('shouldOmitHeaderTitle', true); +provide('shouldHeaderThin', true); - contextmenu() { - return [{ - type: 'label', - text: this.path, - }, { - icon: 'fas fa-expand-alt', - text: this.$ts.showInPage, - action: this.expand, - }, this.sideViewHook ? { - icon: 'fas fa-columns', - text: this.$ts.openInSideView, - action: () => { - this.sideViewHook(this.path); - this.$refs.window.close(); - }, - } : undefined, { - icon: 'fas fa-external-link-alt', - text: this.$ts.popout, - action: this.popout, - }, null, { - icon: 'fas fa-external-link-alt', - text: this.$ts.openInNewTab, - action: () => { - window.open(this.url, '_blank'); - this.$refs.window.close(); - }, - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: () => { - copyToClipboard(this.url); - }, - }]; +const pageUrl = $computed(() => url + path); +const contextmenu = $computed(() => { + return [{ + type: 'label', + text: path, + }, { + icon: 'fas fa-expand-alt', + text: i18n.ts.showInPage, + action: expand, + }, { + icon: 'fas fa-external-link-alt', + text: i18n.ts.popout, + action: popout, + }, null, { + icon: 'fas fa-external-link-alt', + text: i18n.ts.openInNewTab, + action: () => { + window.open(pageUrl, '_blank'); + modal.close(); }, - }, - - methods: { - changePage(page) { - if (page == null) return; - if (page[symbols.PAGE_INFO]) { - this.pageInfo = page[symbols.PAGE_INFO]; - } + }, { + icon: 'fas fa-link', + text: i18n.ts.copyLink, + action: () => { + copyToClipboard(pageUrl); }, + }]; +}); - navigate(path, record = true) { - if (record) this.history.push(this.path); - this.path = path; - const { component, props } = resolve(path); - this.component = component; - this.props = props; - }, +function navigate(path, record = true) { + if (record) history.push(router.getCurrentPath()); + router.push(path); +} - back() { - this.navigate(this.history.pop(), false); - }, +function back() { + navigate(history.pop(), false); +} - expand() { - this.$router.push(this.path); - this.$refs.window.close(); - }, +function expand() { + mainRouter.push(path); + modal.close(); +} - popout() { - popout(this.path, this.$el); - this.$refs.window.close(); - }, +function popout() { + _popout(path, rootEl); + modal.close(); +} - onContextmenu(ev: MouseEvent) { - os.contextMenu(this.contextmenu, ev); - }, - }, -}); +function onContextmenu(ev: MouseEvent) { + os.contextMenu(contextmenu, ev); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 9ec1e53c1e..c2c92f541d 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -225,7 +225,7 @@ function undoReact(note): void { }); } -const currentClipPage = inject<Ref<misskey.entities.Clip>>('currentClipPage'); +const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null); function onContextmenu(ev: MouseEvent): void { const isLink = (el: HTMLElement) => { diff --git a/packages/client/src/components/page-window.vue b/packages/client/src/components/page-window.vue index 7455236bad..7de09d3be4 100644 --- a/packages/client/src/components/page-window.vue +++ b/packages/client/src/components/page-window.vue @@ -1,186 +1,135 @@ <template> -<XWindow ref="window" +<XWindow + ref="windowEl" :initial-width="500" :initial-height="500" :can-resize="true" :close-button="true" + :buttons-left="buttonsLeft" + :buttons-right="buttonsRight" :contextmenu="contextmenu" @closed="$emit('closed')" > <template #header> - <template v-if="pageInfo"> - <i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i> - <span>{{ pageInfo.title }}</span> + <template v-if="pageMetadata?.value"> + <i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i> + <span>{{ pageMetadata.value.title }}</span> </template> </template> - <template #headerLeft> - <button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button> - </template> - <template #headerRight> - <button v-tooltip="$ts.showInPage" class="_button" @click="expand()"><i class="fas fa-expand-alt"></i></button> - <button v-tooltip="$ts.popout" class="_button" @click="popout()"><i class="fas fa-external-link-alt"></i></button> - <button class="_button" @click="menu"><i class="fas fa-ellipsis-h"></i></button> - </template> - <div class="yrolvcoq" :style="{ background: pageInfo?.bg }"> - <MkStickyContainer> - <template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template> - <component :is="component" v-bind="props" :ref="changePage"/> - </MkStickyContainer> + <div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }"> + <RouterView :router="router"/> </div> </XWindow> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ComputedRef, inject, provide } from 'vue'; +import RouterView from './global/router-view.vue'; import XWindow from '@/components/ui/window.vue'; -import { popout } from '@/scripts/popout'; +import { popout as _popout } from '@/scripts/popout'; import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { resolve } from '@/router'; import { url } from '@/config'; -import * as symbols from '@/symbols'; import * as os from '@/os'; +import { mainRouter, routes } from '@/router'; +import { Router } from '@/nirax'; +import { i18n } from '@/i18n'; +import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; -export default defineComponent({ - components: { - XWindow, - }, +const props = defineProps<{ + initialPath: string; +}>(); - inject: { - sideViewHook: { - default: null - } - }, +defineEmits<{ + (ev: 'closed'): void; +}>(); - provide() { - return { - navHook: (path) => { - this.navigate(path); - }, - shouldHeaderThin: true, - }; - }, +const router = new Router(routes, props.initialPath); - props: { - initialPath: { - type: String, - required: true, - }, - initialComponent: { - type: Object, - required: true, - }, - initialProps: { - type: Object, - required: false, - default: () => {}, - }, - }, +let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); +let windowEl = $ref<InstanceType<typeof XWindow>>(); +const history = $ref<string[]>([props.initialPath]); +const buttonsLeft = $computed(() => { + const buttons = []; - emits: ['closed'], + if (history.length > 1) { + buttons.push({ + icon: 'fas fa-arrow-left', + onClick: back, + }); + } - data() { - return { - pageInfo: null, - path: this.initialPath, - component: this.initialComponent, - props: this.initialProps, - history: [], - }; - }, + return buttons; +}); +const buttonsRight = $computed(() => { + const buttons = [{ + icon: 'fas fa-expand-alt', + title: i18n.ts.showInPage, + onClick: expand, + }]; - computed: { - url(): string { - return url + this.path; - }, + return buttons; +}); - contextmenu() { - return [{ - type: 'label', - text: this.path, - }, { - icon: 'fas fa-expand-alt', - text: this.$ts.showInPage, - action: this.expand - }, this.sideViewHook ? { - icon: 'fas fa-columns', - text: this.$ts.openInSideView, - action: () => { - this.sideViewHook(this.path); - this.$refs.window.close(); - } - } : undefined, { - icon: 'fas fa-external-link-alt', - text: this.$ts.popout, - action: this.popout - }, null, { - icon: 'fas fa-external-link-alt', - text: this.$ts.openInNewTab, - action: () => { - window.open(this.url, '_blank'); - this.$refs.window.close(); - } - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: () => { - copyToClipboard(this.url); - } - }]; - }, - }, +router.addListener('push', ctx => { + history.push(router.getCurrentPath()); +}); - methods: { - changePage(page) { - if (page == null) return; - if (page[symbols.PAGE_INFO]) { - this.pageInfo = page[symbols.PAGE_INFO]; - } - }, +provide('router', router); +provideMetadataReceiver((info) => { + pageMetadata = info; +}); +provide('shouldOmitHeaderTitle', true); +provide('shouldHeaderThin', true); - navigate(path, record = true) { - if (record) this.history.push(this.path); - this.path = path; - const { component, props } = resolve(path); - this.component = component; - this.props = props; - }, +const contextmenu = $computed(() => ([{ + icon: 'fas fa-expand-alt', + text: i18n.ts.showInPage, + action: expand, +}, { + icon: 'fas fa-external-link-alt', + text: i18n.ts.popout, + action: popout, +}, { + icon: 'fas fa-external-link-alt', + text: i18n.ts.openInNewTab, + action: () => { + window.open(url + router.getCurrentPath(), '_blank'); + windowEl.close(); + }, +}, { + icon: 'fas fa-link', + text: i18n.ts.copyLink, + action: () => { + copyToClipboard(url + router.getCurrentPath()); + }, +}])); + +function menu(ev) { + os.popupMenu(contextmenu, ev.currentTarget ?? ev.target); +} - menu(ev) { - os.popupMenu([{ - icon: 'fas fa-external-link-alt', - text: this.$ts.openInNewTab, - action: () => { - window.open(this.url, '_blank'); - this.$refs.window.close(); - } - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: () => { - copyToClipboard(this.url); - } - }], ev.currentTarget ?? ev.target); - }, +function back() { + history.pop(); + router.change(history[history.length - 1]); +} - back() { - this.navigate(this.history.pop(), false); - }, +function close() { + windowEl.close(); +} - close() { - this.$refs.window.close(); - }, +function expand() { + mainRouter.push(router.getCurrentPath()); + windowEl.close(); +} - expand() { - this.$router.push(this.path); - this.$refs.window.close(); - }, +function popout() { + _popout(router.getCurrentPath(), windowEl.$el); + windowEl.close(); +} - popout() { - popout(this.path, this.$el); - this.$refs.window.close(); - }, - }, +defineExpose({ + close, }); </script> diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue index 2066cf579d..3cd4378f03 100644 --- a/packages/client/src/components/ui/window.vue +++ b/packages/client/src/components/ui/window.vue @@ -4,14 +4,14 @@ <div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown"> <div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu"> <span class="left"> - <slot name="headerLeft"></slot> + <button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button> </span> <span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown"> <slot name="header"></slot> </span> <span class="right"> - <slot name="headerRight"></slot> - <button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button> + <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="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button> </span> </div> <div v-if="padding" class="body"> @@ -46,41 +46,41 @@ const minHeight = 50; const minWidth = 250; function dragListen(fn) { - window.addEventListener('mousemove', fn); - window.addEventListener('touchmove', fn); + window.addEventListener('mousemove', fn); + window.addEventListener('touchmove', fn); window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); - window.addEventListener('touchend', dragClear.bind(null, fn)); + window.addEventListener('mouseup', dragClear.bind(null, fn)); + window.addEventListener('touchend', dragClear.bind(null, fn)); } function dragClear(fn) { - window.removeEventListener('mousemove', fn); - window.removeEventListener('touchmove', fn); + window.removeEventListener('mousemove', fn); + window.removeEventListener('touchmove', fn); window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); - window.removeEventListener('touchend', dragClear); + window.removeEventListener('mouseup', dragClear); + window.removeEventListener('touchend', dragClear); } export default defineComponent({ provide: { - inWindow: true + inWindow: true, }, props: { padding: { type: Boolean, required: false, - default: false + default: false, }, initialWidth: { type: Number, required: false, - default: 400 + default: 400, }, initialHeight: { type: Number, required: false, - default: null + default: null, }, canResize: { type: Boolean, @@ -105,7 +105,17 @@ export default defineComponent({ contextmenu: { type: Array, required: false, - } + }, + buttonsLeft: { + type: Array, + required: false, + default: [], + }, + buttonsRight: { + type: Array, + required: false, + default: [], + }, }, emits: ['closed'], @@ -162,7 +172,10 @@ export default defineComponent({ this.top(); }, - onHeaderMousedown(evt) { + onHeaderMousedown(evt: MouseEvent) { + // 右クリックはコンテキストメニューを開こうとした可能性が高いため無視 + if (evt.button === 2) return; + const main = this.$el as any; if (!contains(main, document.activeElement)) main.focus(); @@ -356,12 +369,12 @@ export default defineComponent({ 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; // 上はみ出し - } - } + 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; // 上はみ出し + }, + }, }); </script> @@ -404,17 +417,25 @@ export default defineComponent({ border-bottom: solid 1px var(--divider); > .left, > .right { - > ::v-deep(button) { + > .button { height: var(--height); width: var(--height); &:hover { color: var(--fgHighlighted); } + + &.highlighted { + color: var(--accent); + } } } > .left { + margin-right: 16px; + } + + > .right { min-width: 16px; } |