summaryrefslogtreecommitdiff
path: root/packages/client/src/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-06-20 17:38:49 +0900
committerGitHub <noreply@github.com>2022-06-20 17:38:49 +0900
commit699f24f3dcdb156838eb70602885c0b2cdd02cbc (patch)
tree45b28eeadbb7d9e7f3847bd04f75ed010153619a /packages/client/src/components
parentrefactor: チャットルームをComposition API化 (#8850) (diff)
downloadsharkey-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.vue94
-rw-r--r--packages/client/src/components/drive-file-thumbnail.vue2
-rw-r--r--packages/client/src/components/form/folder.vue4
-rw-r--r--packages/client/src/components/global/a.vue31
-rw-r--r--packages/client/src/components/global/header.vue361
-rw-r--r--packages/client/src/components/global/page-header.vue300
-rw-r--r--packages/client/src/components/global/router-view.vue39
-rw-r--r--packages/client/src/components/index.ts9
-rw-r--r--packages/client/src/components/modal-page-window.vue207
-rw-r--r--packages/client/src/components/note.vue2
-rw-r--r--packages/client/src/components/page-window.vue245
-rw-r--r--packages/client/src/components/ui/window.vue69
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;
}