94 lines
		
	
	
		
			2.1 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			94 lines
		
	
	
		
			2.1 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <!--
 | |
| SPDX-FileCopyrightText: syuilo and misskey-project
 | |
| SPDX-License-Identifier: AGPL-3.0-only
 | |
| -->
 | |
| 
 | |
| <template>
 | |
| <div ref="el" :class="$style.root">
 | |
| 	<MkMenu :items="items" :align="align" :width="width" :asDrawer="false" @close="onChildClosed"/>
 | |
| </div>
 | |
| </template>
 | |
| 
 | |
| <script lang="ts" setup>
 | |
| import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
 | |
| import MkMenu from './MkMenu.vue';
 | |
| import type { MenuItem } from '@/types/menu.js';
 | |
| 
 | |
| const props = defineProps<{
 | |
| 	items: MenuItem[];
 | |
| 	targetElement: HTMLElement;
 | |
| 	rootElement: HTMLElement;
 | |
| 	width?: number;
 | |
| }>();
 | |
| 
 | |
| const emit = defineEmits<{
 | |
| 	(ev: 'closed'): void;
 | |
| 	(ev: 'actioned'): void;
 | |
| }>();
 | |
| 
 | |
| provide('isNestingMenu', true);
 | |
| 
 | |
| const el = shallowRef<HTMLElement>();
 | |
| const align = 'left';
 | |
| 
 | |
| const SCROLLBAR_THICKNESS = 16;
 | |
| 
 | |
| function setPosition() {
 | |
| 	if (el.value == null) return;
 | |
| 	const rootRect = props.rootElement.getBoundingClientRect();
 | |
| 	const parentRect = props.targetElement.getBoundingClientRect();
 | |
| 	const myRect = el.value.getBoundingClientRect();
 | |
| 
 | |
| 	let left = props.targetElement.offsetWidth;
 | |
| 	let top = (parentRect.top - rootRect.top) - 8;
 | |
| 	if (rootRect.left + left + myRect.width >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
 | |
| 		left = -myRect.width;
 | |
| 	}
 | |
| 	if (rootRect.top + top + myRect.height >= (window.innerHeight - SCROLLBAR_THICKNESS)) {
 | |
| 		top = top - ((rootRect.top + top + myRect.height) - (window.innerHeight - SCROLLBAR_THICKNESS));
 | |
| 	}
 | |
| 	el.value.style.left = left + 'px';
 | |
| 	el.value.style.top = top + 'px';
 | |
| }
 | |
| 
 | |
| function onChildClosed(actioned?: boolean) {
 | |
| 	if (actioned) {
 | |
| 		emit('actioned');
 | |
| 	} else {
 | |
| 		emit('closed');
 | |
| 	}
 | |
| }
 | |
| 
 | |
| watch(() => props.targetElement, () => {
 | |
| 	setPosition();
 | |
| });
 | |
| 
 | |
| const ro = new ResizeObserver((entries, observer) => {
 | |
| 	setPosition();
 | |
| });
 | |
| 
 | |
| onMounted(() => {
 | |
| 	if (el.value) ro.observe(el.value);
 | |
| 	setPosition();
 | |
| 	nextTick(() => {
 | |
| 		setPosition();
 | |
| 	});
 | |
| });
 | |
| 
 | |
| onUnmounted(() => {
 | |
| 	ro.disconnect();
 | |
| });
 | |
| 
 | |
| defineExpose({
 | |
| 	checkHit: (ev: MouseEvent) => {
 | |
| 		return (ev.target === el.value || el.value?.contains(ev.target as Node));
 | |
| 	},
 | |
| });
 | |
| </script>
 | |
| 
 | |
| <style lang="scss" module>
 | |
| .root {
 | |
| 	position: absolute;
 | |
| }
 | |
| </style>
 | 
