enhance(client): improve modal menu for mobile

This commit is contained in:
syuilo 2021-12-17 02:14:40 +09:00
parent 6d6162333e
commit 6ab2c7780c
10 changed files with 340 additions and 393 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<MkModal ref="modal" :front="true" @click="done(true)" @closed="$emit('closed')"> <MkModal ref="modal" :prefer-type="'dialog'" :front="true" @click="done(true)" @closed="$emit('closed')">
<div class="mk-dialog"> <div class="mk-dialog">
<div v-if="icon" class="icon"> <div v-if="icon" class="icon">
<i :class="icon"></i> <i :class="icon"></i>

View File

@ -1,17 +1,17 @@
<template> <template>
<MkPopup ref="popup" v-slot="{ point, close }" :manual-showing="manualShowing" :src="src" :front="true" @click="close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')"> <MkModal ref="modal" v-slot="{ type, maxHeight }" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
<MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ pointer: point === 'top' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/> <MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ drawer: type === 'drawer' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" :as-drawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen"/>
</MkPopup> </MkModal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, markRaw } from 'vue'; import { defineComponent, markRaw } from 'vue';
import MkPopup from '@/components/ui/popup.vue'; import MkModal from '@/components/ui/modal.vue';
import MkEmojiPicker from '@/components/emoji-picker.vue'; import MkEmojiPicker from '@/components/emoji-picker.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
MkPopup, MkModal,
MkEmojiPicker, MkEmojiPicker,
}, },
@ -44,7 +44,7 @@ export default defineComponent({
methods: { methods: {
chosen(emoji: any) { chosen(emoji: any) {
this.$emit('done', emoji); this.$emit('done', emoji);
this.$refs.popup.close(); this.$refs.modal.close();
}, },
opening() { opening() {
@ -57,20 +57,10 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.ryghynhb { .ryghynhb {
&.pointer { &.drawer {
&:before { border-radius: 24px;
--size: 8px; border-bottom-right-radius: 0;
content: ''; border-bottom-left-radius: 0;
display: block;
position: absolute;
top: calc(0px - (var(--size) * 2));
left: 0;
right: 0;
width: 0;
margin: auto;
border: solid var(--size) transparent;
border-bottom-color: var(--popup);
}
} }
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="omfetrab" :class="['w' + width, 'h' + height, { big }]"> <div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : null }">
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()"> <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
<div ref="emojis" class="emojis"> <div ref="emojis" class="emojis">
<section class="result"> <section class="result">
@ -92,9 +92,17 @@ export default defineComponent({
props: { props: {
showPinned: { showPinned: {
required: false, required: false,
default: true default: true,
}, },
asReactionPicker: { asReactionPicker: {
required: false,
},
maxHeight: {
type: Number,
required: false,
},
asDrawer: {
type: Boolean,
required: false required: false
}, },
}, },
@ -353,26 +361,53 @@ export default defineComponent({
&.w1 { &.w1 {
width: calc((var(--eachSize) * 5) + (#{$pad} * 2)); width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
--columns: 1fr 1fr 1fr 1fr 1fr;
} }
&.w2 { &.w2 {
width: calc((var(--eachSize) * 6) + (#{$pad} * 2)); width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
--columns: 1fr 1fr 1fr 1fr 1fr 1fr;
} }
&.w3 { &.w3 {
width: calc((var(--eachSize) * 7) + (#{$pad} * 2)); width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
--columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
} }
&.h1 { &.h1 {
--height: calc((var(--eachSize) * 4) + (#{$pad} * 2)); height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
} }
&.h2 { &.h2 {
--height: calc((var(--eachSize) * 6) + (#{$pad} * 2)); height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
} }
&.h3 { &.h3 {
--height: calc((var(--eachSize) * 8) + (#{$pad} * 2)); height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
}
&.asDrawer {
width: 100% !important;
> .emojis {
::v-deep(section) {
> div {
display: grid;
grid-template-columns: var(--columns);
> button {
aspect-ratio: 1 / 1;
width: auto;
height: auto;
min-width: 0;
> * {
font-size: 30px;
}
}
}
}
}
} }
> .search { > .search {
@ -409,7 +444,7 @@ export default defineComponent({
} }
> .emojis { > .emojis {
height: var(--height); height: 100%;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;

View File

@ -1,5 +1,5 @@
<template> <template>
<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')"> <MkModal ref="modal" :prefer-type="'dialog'" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="szkkfdyq _popup"> <div class="szkkfdyq _popup">
<div class="main"> <div class="main">
<template v-for="item in items"> <template v-for="item in items">

View File

@ -1,5 +1,5 @@
<template> <template>
<MkModal ref="modal" :position="'top'" @click="$refs.modal.close()" @closed="$emit('closed')"> <MkModal ref="modal" :prefer-type="'dialog:top'" @click="$refs.modal.close()" @closed="$emit('closed')">
<MkPostForm v-bind="$attrs" @posted="$refs.modal.close()" @cancel="$refs.modal.close()" @esc="$refs.modal.close()"/> <MkPostForm v-bind="$attrs" @posted="$refs.modal.close()" @cancel="$refs.modal.close()" @esc="$refs.modal.close()"/>
</MkModal> </MkModal>
</template> </template>

View File

@ -1,8 +1,8 @@
<template> <template>
<div ref="items" v-hotkey="keymap" <div ref="items" v-hotkey="keymap"
class="rrevdjwt" class="rrevdjwt"
:class="{ center: align === 'center' }" :class="{ center: align === 'center', asDrawer }"
:style="{ width: width ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }" :style="{ width: (width && !asDrawer) ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }"
@contextmenu.self="e => e.preventDefault()" @contextmenu.self="e => e.preventDefault()"
> >
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
@ -56,6 +56,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
required: false required: false
}, },
asDrawer: {
type: Boolean,
required: false
},
align: { align: {
type: String, type: String,
requried: false requried: false
@ -279,5 +283,29 @@ export default defineComponent({
height: 1px; height: 1px;
background: var(--divider); background: var(--divider);
} }
&.asDrawer {
padding: 12px 0;
width: 100%;
> .item {
font-size: 1em;
padding: 12px 24px;
&:before {
width: calc(100% - 24px);
border-radius: 12px;
}
> i {
margin-right: 14px;
width: 24px;
}
}
> .divider {
margin: 12px 0;
}
}
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> <MkModal ref="modal" :prefer-type="'dialog'" @click="$emit('click')" @closed="$emit('closed')">
<div class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown"> <div class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
<div class="header"> <div class="header">
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="fas fa-times"></i></button> <button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="fas fa-times"></i></button>

View File

@ -1,17 +1,18 @@
<template> <template>
<transition :name="$store.state.animation ? popup ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? popup ? 500 : 300 : 0" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered"> <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="$emit('closed')" @enter="$emit('opening')" @after-enter="childRendered">
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="bg _modalBg" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> <div class="bg _modalBg" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" class="content" :class="{ popup, fixed, top: position === 'top' }" :style="{ zIndex }" @click.self="onBgClick"> <div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
<slot></slot> <slot :max-height="maxHeight" :type="type"></slot>
</div> </div>
</div> </div>
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, nextTick, onMounted, computed, PropType, ref, watch } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
function getFixedContainer(el: Element | null): Element | null { function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === 'BODY') return null; if (el == null || el.tagName === 'BODY') return null;
@ -27,6 +28,7 @@ export default defineComponent({
provide: { provide: {
modal: true modal: true
}, },
props: { props: {
manualShowing: { manualShowing: {
type: Boolean, type: Boolean,
@ -38,61 +40,81 @@ export default defineComponent({
required: false required: false
}, },
src: { src: {
type: Object as PropType<HTMLElement>,
required: false, required: false,
default: null,
}, },
position: { preferType: {
required: false required: false,
type: String,
default: 'auto',
}, },
front: { front: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}
}, },
noOverlap: {
type: Boolean,
required: false,
default: true,
},
},
emits: ['opening', 'click', 'esc', 'close', 'closed'], emits: ['opening', 'click', 'esc', 'close', 'closed'],
data() {
return { setup(props, context) {
zIndex: os.claimZIndex(this.front), const maxHeight = ref<number>();
showing: true, const fixed = ref(false);
fixed: false, const transformOrigin = ref('center');
transformOrigin: 'center', const showing = ref(true);
contentClicking: false, const content = ref<HTMLElement>();
}; const zIndex = os.claimZIndex(props.front);
}, const type = computed(() => {
computed: { if (props.preferType === 'auto') {
keymap(): any { if (isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) {
return { return 'drawer';
'esc': () => this.$emit('esc'), } else {
}; return props.src != null ? 'popup' : 'dialog';
}, }
popup(): boolean { } else {
return this.src != null; return props.preferType;
} }
},
mounted() {
this.$watch('src', () => {
this.fixed = getFixedContainer(this.src) != null;
this.$nextTick(() => {
this.align();
}); });
}, { immediate: true });
this.$nextTick(() => { let contentClicking = false;
const popover = this.$refs.content as any;
new ResizeObserver((entries, observer) => {
this.align();
}).observe(popover);
});
},
methods: {
align() {
if (!this.popup) return;
const popover = this.$refs.content as any; const close = () => {
// eslint-disable-next-line vue/no-mutating-props
if (props.src) props.src.style.pointerEvents = 'auto';
showing.value = false;
context.emit('close');
};
const onBgClick = () => {
if (contentClicking) return;
context.emit('click');
};
if (type.value === 'drawer') {
maxHeight.value = 300;
}
const keymap = {
'esc': () => context.emit('esc'),
};
const MARGIN = 16;
const align = () => {
if (props.src == null) return;
if (type.value === 'drawer') return;
const popover = content.value!;
if (popover == null) return; if (popover == null) return;
const rect = this.src.getBoundingClientRect(); const rect = props.src.getBoundingClientRect();
const width = popover.offsetWidth; const width = popover.offsetWidth;
const height = popover.offsetHeight; const height = popover.offsetHeight;
@ -100,102 +122,143 @@ export default defineComponent({
let left; let left;
let top; let top;
if (this.srcCenter) { if (props.srcCenter) {
const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2); const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2);
left = (x - (width / 2)); left = (x - (width / 2));
top = (y - (height / 2)); top = (y - (height / 2));
} else { } else {
const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight; const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight;
left = (x - (width / 2)); left = (x - (width / 2));
top = y; top = y;
} }
if (this.fixed) { if (fixed.value) {
//
if (left + width > window.innerWidth) { if (left + width > window.innerWidth) {
left = window.innerWidth - width; left = window.innerWidth - width;
} }
if (top + height > window.innerHeight) { //
top = window.innerHeight - height; if (top + height > (window.innerHeight - MARGIN)) {
if (props.noOverlap) {
const underSpace = (window.innerHeight - MARGIN) - top;
const upperSpace = (rect.top - MARGIN);
if (underSpace >= (upperSpace / 3)) {
maxHeight.value = underSpace;
} else {
maxHeight.value = upperSpace;
top = (upperSpace + MARGIN) - height;
} }
} else { } else {
top = (window.innerHeight - MARGIN) - height;
}
}
} else {
//
if (left + width - window.pageXOffset > window.innerWidth) { if (left + width - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - width + window.pageXOffset - 1; left = window.innerWidth - width + window.pageXOffset - 1;
} }
if (top + height - window.pageYOffset > window.innerHeight) { //
top = window.innerHeight - height + window.pageYOffset - 1; if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
if (props.noOverlap) {
const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
const upperSpace = (rect.top - MARGIN);
if (underSpace >= (upperSpace / 3)) {
maxHeight.value = underSpace;
} else {
maxHeight.value = upperSpace;
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
}
} else {
top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
}
} }
} }
if (top < 0) { if (top < 0) {
top = 0; top = MARGIN;
} }
if (left < 0) { if (left < 0) {
left = 0; left = 0;
} }
if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) { if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) {
this.transformOrigin = 'center top'; transformOrigin.value = 'center top';
} else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) {
transformOrigin.value = 'center bottom';
} else { } else {
this.transformOrigin = 'center'; transformOrigin.value = 'center';
} }
popover.style.left = left + 'px'; popover.style.left = left + 'px';
popover.style.top = top + 'px'; popover.style.top = top + 'px';
}, };
childRendered() { const childRendered = () => {
// //
const content = this.$refs.content.children[0]; const el = content.value!.children[0];
content.addEventListener('mousedown', e => { el.addEventListener('mousedown', e => {
this.contentClicking = true; contentClicking = true;
window.addEventListener('mouseup', e => { window.addEventListener('mouseup', e => {
// click mouseup // click mouseup
setTimeout(() => { setTimeout(() => {
this.contentClicking = false; contentClicking = false;
}, 100); }, 100);
}, { passive: true, once: true }); }, { passive: true, once: true });
}, { passive: true }); }, { passive: true });
}, };
close() { onMounted(() => {
this.showing = false; watch(() => props.src, async () => {
this.$emit('close'); if (props.src) {
}, // eslint-disable-next-line vue/no-mutating-props
props.src.style.pointerEvents = 'none';
onBgClick() {
if (this.contentClicking) return;
this.$emit('click');
},
onClosed() {
this.$emit('closed');
}
} }
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
await nextTick()
align();
}, { immediate: true, });
nextTick(() => {
const popover = content.value;
new ResizeObserver((entries, observer) => {
align();
}).observe(popover!);
});
});
return {
showing,
type,
fixed,
content,
transformOrigin,
maxHeight,
close,
zIndex,
keymap,
onBgClick,
childRendered,
};
},
}); });
</script> </script>
<style lang="scss">
.modal-popup-enter-active, .modal-popup-leave-active,
.modal-enter-from, .modal-leave-to {
> .content {
transform-origin: var(--transformOrigin);
}
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.modal-enter-active, .modal-leave-active { .modal-enter-active, .modal-leave-active {
> .bg { > .bg {
transition: opacity 0.3s !important; transition: opacity 0.2s !important;
} }
> .content { > .content {
transition: opacity 0.3s, transform 0.3s !important; transform-origin: var(--transformOrigin);
transition: opacity 0.2s, transform 0.2s !important;
} }
} }
.modal-enter-from, .modal-leave-to { .modal-enter-from, .modal-leave-to {
@ -206,17 +269,19 @@ export default defineComponent({
> .content { > .content {
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
transform-origin: var(--transformOrigin);
transform: scale(0.9); transform: scale(0.9);
} }
} }
.modal-popup-enter-active, .modal-popup-leave-active { .modal-popup-enter-active, .modal-popup-leave-active {
> .bg { > .bg {
transition: opacity 0.3s !important; transition: opacity 0.2s !important;
} }
> .content { > .content {
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1) !important; transform-origin: var(--transformOrigin);
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
} }
} }
.modal-popup-enter-from, .modal-popup-leave-to { .modal-popup-enter-from, .modal-popup-leave-to {
@ -227,12 +292,43 @@ export default defineComponent({
> .content { > .content {
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
transform-origin: var(--transformOrigin);
transform: scale(0.9); transform: scale(0.9);
} }
} }
.modal-drawer-enter-active {
> .bg {
transition: opacity 0.2s !important;
}
> .content {
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.modal-drawer-leave-active {
> .bg {
transition: opacity 0.2s !important;
}
> .content {
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.modal-drawer-enter-from, .modal-drawer-leave-to {
> .bg {
opacity: 0;
}
> .content {
pointer-events: none;
transform: translateY(100%);
}
}
.qzhlnise { .qzhlnise {
> .content:not(.popup) { &.dialog {
> .content {
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -262,8 +358,10 @@ export default defineComponent({
} }
} }
} }
}
> .content.popup { &.popup {
> .content {
position: absolute; position: absolute;
&.fixed { &.fixed {
@ -271,4 +369,27 @@ export default defineComponent({
} }
} }
} }
&.drawer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: clip;
> .content {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin: auto;
> ::v-deep(*) {
margin: auto;
}
}
}
}
</style> </style>

View File

@ -1,17 +1,17 @@
<template> <template>
<MkPopup ref="popup" v-slot="{ maxHeight, close }" :src="src" @closed="$emit('closed')"> <MkModal ref="modal" v-slot="{ type, maxHeight }" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" class="_popup _shadow" @close="close()"/> <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="$refs.modal.close()"/>
</MkPopup> </MkModal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import MkPopup from './popup.vue'; import MkModal from './modal.vue';
import MkMenu from './menu.vue'; import MkMenu from './menu.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
MkPopup, MkModal,
MkMenu, MkMenu,
}, },
@ -40,3 +40,13 @@ export default defineComponent({
emits: ['close', 'closed'], emits: ['close', 'closed'],
}); });
</script> </script>
<style lang="scss" scoped>
.sfhdhdhq {
&.drawer {
border-radius: 24px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}
</style>

View File

@ -1,237 +0,0 @@
<template>
<transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="$emit('closed')" @enter="$emit('opening')">
<div v-show="manualShowing != null ? manualShowing : showing" ref="content" class="ccczpooj" :class="{ fixed, top: position === 'top' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<slot :max-height="maxHeight" :close="close"></slot>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent, nextTick, onMounted, onUnmounted, PropType, ref, watch } from 'vue';
import * as os from '@/os';
function getFixedContainer(el: Element | null | undefined): Element | null {
if (el == null || el.tagName === 'BODY') return null;
const position = window.getComputedStyle(el).getPropertyValue('position');
if (position === 'fixed') {
return el;
} else {
return getFixedContainer(el.parentElement);
}
}
export default defineComponent({
props: {
manualShowing: {
type: Boolean,
required: false,
default: null,
},
srcCenter: {
type: Boolean,
required: false
},
src: {
type: Object as PropType<HTMLElement>,
required: false,
},
position: {
required: false
},
front: {
type: Boolean,
required: false,
default: false,
},
noOverlap: {
type: Boolean,
required: false,
default: true,
},
},
emits: ['opening', 'click', 'esc', 'close', 'closed'],
setup(props, context) {
const maxHeight = ref<number>();
const fixed = ref(false);
const transformOrigin = ref('center');
const showing = ref(true);
const content = ref<HTMLElement>();
const zIndex = os.claimZIndex(props.front);
const close = () => {
// eslint-disable-next-line vue/no-mutating-props
if (props.src) props.src.style.pointerEvents = 'auto';
showing.value = false;
context.emit('close');
};
const MARGIN = 16;
const align = () => {
if (props.src == null) return;
const popover = content.value!;
if (popover == null) return;
const rect = props.src.getBoundingClientRect();
const width = popover.offsetWidth;
const height = popover.offsetHeight;
let left;
let top;
if (props.srcCenter) {
const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2);
left = (x - (width / 2));
top = (y - (height / 2));
} else {
const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight;
left = (x - (width / 2));
top = y;
}
if (fixed.value) {
//
if (left + width > window.innerWidth) {
left = window.innerWidth - width;
}
//
if (top + height > (window.innerHeight - MARGIN)) {
if (props.noOverlap) {
const underSpace = (window.innerHeight - MARGIN) - top;
const upperSpace = (rect.top - MARGIN);
if (underSpace >= (upperSpace / 3)) {
maxHeight.value = underSpace;
} else {
maxHeight.value = upperSpace;
top = (upperSpace + MARGIN) - height;
}
} else {
top = (window.innerHeight - MARGIN) - height;
}
}
} else {
//
if (left + width - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - width + window.pageXOffset - 1;
}
//
if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
if (props.noOverlap) {
const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
const upperSpace = (rect.top - MARGIN);
if (underSpace >= (upperSpace / 3)) {
maxHeight.value = underSpace;
} else {
maxHeight.value = upperSpace;
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
}
} else {
top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
}
}
}
if (top < 0) {
top = MARGIN;
}
if (left < 0) {
left = 0;
}
if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) {
transformOrigin.value = 'center top';
} else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) {
transformOrigin.value = 'center bottom';
} else {
transformOrigin.value = 'center';
}
popover.style.left = left + 'px';
popover.style.top = top + 'px';
};
const onDocumentClick = (ev: MouseEvent) => {
const flyoutElement = content.value;
let targetElement = ev.target;
do {
if (targetElement === flyoutElement) {
return;
}
targetElement = targetElement.parentNode;
} while (targetElement);
close();
};
onMounted(() => {
watch(() => props.src, async () => {
if (props.src) {
// eslint-disable-next-line vue/no-mutating-props
props.src.style.pointerEvents = 'none';
}
fixed.value = getFixedContainer(props.src) != null;
await nextTick()
align();
}, { immediate: true, });
nextTick(() => {
const popover = content.value;
new ResizeObserver((entries, observer) => {
align();
}).observe(popover!);
});
document.addEventListener('mousedown', onDocumentClick, { passive: true });
onUnmounted(() => {
document.removeEventListener('mousedown', onDocumentClick);
});
});
return {
showing,
fixed,
content,
transformOrigin,
maxHeight,
close,
zIndex,
};
},
});
</script>
<style lang="scss" scoped>
.popup-menu-enter-active {
transform-origin: var(--transformOrigin);
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
}
.popup-menu-leave-active {
transform-origin: var(--transformOrigin);
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), transform 0.2s cubic-bezier(0.4, 0, 1, 1) !important;
}
.popup-menu-enter-from, .popup-menu-leave-to {
pointer-events: none;
opacity: 0;
transform: scale(0.9);
}
.ccczpooj {
position: absolute;
&.fixed {
position: fixed;
}
}
</style>