refactor(client): Refine routing (#8846)
This commit is contained in:
		| @@ -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)), | ||||
| 			), | ||||
| 		}], | ||||
| 	}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -57,7 +57,7 @@ const isThumbnailAvailable = computed(() => { | ||||
| .zdjebgpv { | ||||
| 	position: relative; | ||||
| 	display: flex; | ||||
| 	background: #e1e1e1; | ||||
| 	background: var(--panel); | ||||
| 	border-radius: 8px; | ||||
| 	overflow: clip; | ||||
|  | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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> | ||||
							
								
								
									
										300
									
								
								packages/client/src/components/global/page-header.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								packages/client/src/components/global/page-header.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
							
								
								
									
										39
									
								
								packages/client/src/components/global/router-view.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								packages/client/src/components/global/router-view.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
| @@ -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; | ||||
| 	} | ||||
|   | ||||
| @@ -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, | ||||
| 		}, | ||||
| 	}, | ||||
| defineEmits<{ | ||||
| 	(ev: 'closed'): void; | ||||
| 	(ev: 'click'): 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: () => {}, | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	emits: ['closed'], | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			width: 860, | ||||
| 			height: 660, | ||||
| 			pageInfo: null, | ||||
| 			path: this.initialPath, | ||||
| 			component: this.initialComponent, | ||||
| 			props: this.initialProps, | ||||
| 			history: [], | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		url(): string { | ||||
| 			return url + this.path; | ||||
| 		}, | ||||
|  | ||||
| 		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); | ||||
| 				}, | ||||
| 			}]; | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		changePage(page) { | ||||
| 			if (page == null) return; | ||||
| 			if (page[symbols.PAGE_INFO]) { | ||||
| 				this.pageInfo = page[symbols.PAGE_INFO]; | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		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; | ||||
| 		}, | ||||
|  | ||||
| 		back() { | ||||
| 			this.navigate(this.history.pop(), false); | ||||
| 		}, | ||||
|  | ||||
| 		expand() { | ||||
| 			this.$router.push(this.path); | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
|  | ||||
| 		popout() { | ||||
| 			popout(this.path, this.$el); | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
|  | ||||
| 		onContextmenu(ev: MouseEvent) { | ||||
| 			os.contextMenu(this.contextmenu, ev); | ||||
| 		}, | ||||
| 	}, | ||||
| router.addListener('push', ctx => { | ||||
| 	 | ||||
| }); | ||||
|  | ||||
| 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 = []; | ||||
|  | ||||
| provide('router', router); | ||||
| provideMetadataReceiver((info) => { | ||||
| 	pageMetadata = info; | ||||
| }); | ||||
| provide('shouldOmitHeaderTitle', true); | ||||
| provide('shouldHeaderThin', true); | ||||
|  | ||||
| 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(); | ||||
| 		}, | ||||
| 	}, { | ||||
| 		icon: 'fas fa-link', | ||||
| 		text: i18n.ts.copyLink, | ||||
| 		action: () => { | ||||
| 			copyToClipboard(pageUrl); | ||||
| 		}, | ||||
| 	}]; | ||||
| }); | ||||
|  | ||||
| function navigate(path, record = true) { | ||||
| 	if (record) history.push(router.getCurrentPath()); | ||||
| 	router.push(path); | ||||
| } | ||||
|  | ||||
| function back() { | ||||
| 	navigate(history.pop(), false); | ||||
| } | ||||
|  | ||||
| function expand() { | ||||
| 	mainRouter.push(path); | ||||
| 	modal.close(); | ||||
| } | ||||
|  | ||||
| function popout() { | ||||
| 	_popout(path, rootEl); | ||||
| 	modal.close(); | ||||
| } | ||||
|  | ||||
| function onContextmenu(ev: MouseEvent) { | ||||
| 	os.contextMenu(contextmenu, ev); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   | ||||
| @@ -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) => { | ||||
|   | ||||
| @@ -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; | ||||
| }>(); | ||||
|  | ||||
| defineEmits<{ | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
|  | ||||
| const router = new Router(routes, props.initialPath); | ||||
|  | ||||
| let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); | ||||
| let windowEl = $ref<InstanceType<typeof XWindow>>(); | ||||
| const history = $ref<string[]>([props.initialPath]); | ||||
| const buttonsLeft = $computed(() => { | ||||
| 	const buttons = []; | ||||
|  | ||||
| 	if (history.length > 1) { | ||||
| 		buttons.push({ | ||||
| 			icon: 'fas fa-arrow-left', | ||||
| 			onClick: back, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	return buttons; | ||||
| }); | ||||
| const buttonsRight = $computed(() => { | ||||
| 	const buttons = [{ | ||||
| 		icon: 'fas fa-expand-alt', | ||||
| 		title: i18n.ts.showInPage, | ||||
| 		onClick: expand, | ||||
| 	}]; | ||||
|  | ||||
| 	return buttons; | ||||
| }); | ||||
|  | ||||
| router.addListener('push', ctx => { | ||||
| 	history.push(router.getCurrentPath()); | ||||
| }); | ||||
|  | ||||
| provide('router', router); | ||||
| provideMetadataReceiver((info) => { | ||||
| 	pageMetadata = info; | ||||
| }); | ||||
| provide('shouldOmitHeaderTitle', true); | ||||
| provide('shouldHeaderThin', true); | ||||
|  | ||||
| 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(); | ||||
| 	}, | ||||
|  | ||||
| 	inject: { | ||||
| 		sideViewHook: { | ||||
| 			default: null | ||||
| 		} | ||||
| }, { | ||||
| 	icon: 'fas fa-link', | ||||
| 	text: i18n.ts.copyLink, | ||||
| 	action: () => { | ||||
| 		copyToClipboard(url + router.getCurrentPath()); | ||||
| 	}, | ||||
| }])); | ||||
|  | ||||
| 	provide() { | ||||
| 		return { | ||||
| 			navHook: (path) => { | ||||
| 				this.navigate(path); | ||||
| 			}, | ||||
| 			shouldHeaderThin: true, | ||||
| 		}; | ||||
| 	}, | ||||
| function menu(ev) { | ||||
| 	os.popupMenu(contextmenu, ev.currentTarget ?? ev.target); | ||||
| } | ||||
|  | ||||
| 	props: { | ||||
| 		initialPath: { | ||||
| 			type: String, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		initialComponent: { | ||||
| 			type: Object, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		initialProps: { | ||||
| 			type: Object, | ||||
| 			required: false, | ||||
| 			default: () => {}, | ||||
| 		}, | ||||
| 	}, | ||||
| function back() { | ||||
| 	history.pop(); | ||||
| 	router.change(history[history.length - 1]); | ||||
| } | ||||
|  | ||||
| 	emits: ['closed'], | ||||
| function close() { | ||||
| 	windowEl.close(); | ||||
| } | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			pageInfo: null, | ||||
| 			path: this.initialPath, | ||||
| 			component: this.initialComponent, | ||||
| 			props: this.initialProps, | ||||
| 			history: [], | ||||
| 		}; | ||||
| 	}, | ||||
| function expand() { | ||||
| 	mainRouter.push(router.getCurrentPath()); | ||||
| 	windowEl.close(); | ||||
| } | ||||
|  | ||||
| 	computed: { | ||||
| 		url(): string { | ||||
| 			return url + this.path; | ||||
| 		}, | ||||
| function popout() { | ||||
| 	_popout(router.getCurrentPath(), windowEl.$el); | ||||
| 	windowEl.close(); | ||||
| } | ||||
|  | ||||
| 		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); | ||||
| 				} | ||||
| 			}]; | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		changePage(page) { | ||||
| 			if (page == null) return; | ||||
| 			if (page[symbols.PAGE_INFO]) { | ||||
| 				this.pageInfo = page[symbols.PAGE_INFO]; | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		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; | ||||
| 		}, | ||||
|  | ||||
| 		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); | ||||
| 		}, | ||||
|  | ||||
| 		back() { | ||||
| 			this.navigate(this.history.pop(), false); | ||||
| 		}, | ||||
|  | ||||
| 		close() { | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
|  | ||||
| 		expand() { | ||||
| 			this.$router.push(this.path); | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
|  | ||||
| 		popout() { | ||||
| 			popout(this.path, this.$el); | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
| 	}, | ||||
| defineExpose({ | ||||
| 	close, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
| 			} | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo