diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-12-17 16:18:29 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-12-17 16:18:29 +0900 |
| commit | ad9e6a4ec5d2ff3a05b59f83ef106574d89ffe39 (patch) | |
| tree | edaf3653e3dfe4882e797ca409a7815cdfdbfcd6 /packages/client/src/components | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.100.0 (diff) | |
| download | misskey-ad9e6a4ec5d2ff3a05b59f83ef106574d89ffe39.tar.gz misskey-ad9e6a4ec5d2ff3a05b59f83ef106574d89ffe39.tar.bz2 misskey-ad9e6a4ec5d2ff3a05b59f83ef106574d89ffe39.zip | |
Merge branch 'develop'
Diffstat (limited to 'packages/client/src/components')
| -rw-r--r-- | packages/client/src/components/dialog.vue | 2 | ||||
| -rw-r--r-- | packages/client/src/components/emoji-picker-dialog.vue | 30 | ||||
| -rw-r--r-- | packages/client/src/components/emoji-picker.vue | 54 | ||||
| -rw-r--r-- | packages/client/src/components/launch-pad.vue | 2 | ||||
| -rw-r--r-- | packages/client/src/components/post-form-dialog.vue | 2 | ||||
| -rw-r--r-- | packages/client/src/components/toast.vue | 7 | ||||
| -rw-r--r-- | packages/client/src/components/ui/menu.vue | 32 | ||||
| -rw-r--r-- | packages/client/src/components/ui/modal-window.vue | 2 | ||||
| -rw-r--r-- | packages/client/src/components/ui/modal.vue | 366 | ||||
| -rw-r--r-- | packages/client/src/components/ui/popup-menu.vue | 20 | ||||
| -rw-r--r-- | packages/client/src/components/ui/popup.vue | 237 | ||||
| -rw-r--r-- | packages/client/src/components/url-preview-popup.vue | 4 | ||||
| -rw-r--r-- | packages/client/src/components/user-preview.vue | 4 |
13 files changed, 365 insertions, 397 deletions
diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue index 4da59393e8..90cac9b5ea 100644 --- a/packages/client/src/components/dialog.vue +++ b/packages/client/src/components/dialog.vue @@ -1,5 +1,5 @@ <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 v-if="icon" class="icon"> <i :class="icon"></i> diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue index c1a9f73bcc..d2a405ef5a 100644 --- a/packages/client/src/components/emoji-picker-dialog.vue +++ b/packages/client/src/components/emoji-picker-dialog.vue @@ -1,17 +1,17 @@ <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')"> - <MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ pointer: point === 'top' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/> -</MkPopup> +<MkModal ref="modal" v-slot="{ type, maxHeight }" :prefer-type="asReactionPicker && $store.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparent-bg="true" :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="{ drawer: type === 'drawer' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" :as-drawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen"/> +</MkModal> </template> <script lang="ts"> 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'; export default defineComponent({ components: { - MkPopup, + MkModal, MkEmojiPicker, }, @@ -44,7 +44,7 @@ export default defineComponent({ methods: { chosen(emoji: any) { this.$emit('done', emoji); - this.$refs.popup.close(); + this.$refs.modal.close(); }, opening() { @@ -57,20 +57,10 @@ export default defineComponent({ <style lang="scss" scoped> .ryghynhb { - &.pointer { - &:before { - --size: 8px; - content: ''; - 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); - } + &.drawer { + border-radius: 24px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } } </style> diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index 180aff87ac..ff450246f9 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -1,5 +1,5 @@ <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()"> <div ref="emojis" class="emojis"> <section class="result"> @@ -92,9 +92,17 @@ export default defineComponent({ props: { showPinned: { required: false, - default: true + default: true, }, asReactionPicker: { + required: false, + }, + maxHeight: { + type: Number, + required: false, + }, + asDrawer: { + type: Boolean, required: false }, }, @@ -353,26 +361,60 @@ export default defineComponent({ &.w1 { width: calc((var(--eachSize) * 5) + (#{$pad} * 2)); + --columns: 1fr 1fr 1fr 1fr 1fr; } &.w2 { width: calc((var(--eachSize) * 6) + (#{$pad} * 2)); + --columns: 1fr 1fr 1fr 1fr 1fr 1fr; } &.w3 { width: calc((var(--eachSize) * 7) + (#{$pad} * 2)); + --columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr; } &.h1 { - --height: calc((var(--eachSize) * 4) + (#{$pad} * 2)); + height: calc((var(--eachSize) * 4) + (#{$pad} * 2)); } &.h2 { - --height: calc((var(--eachSize) * 6) + (#{$pad} * 2)); + height: calc((var(--eachSize) * 6) + (#{$pad} * 2)); } &.h3 { - --height: calc((var(--eachSize) * 8) + (#{$pad} * 2)); + height: calc((var(--eachSize) * 8) + (#{$pad} * 2)); + } + + &.asDrawer { + width: 100% !important; + + > .emojis { + ::v-deep(section) { + > header { + height: 32px; + line-height: 32px; + padding: 0 12px; + font-size: 15px; + } + + > div { + display: grid; + grid-template-columns: var(--columns); + + > button { + aspect-ratio: 1 / 1; + width: auto; + height: auto; + min-width: 0; + + > * { + font-size: 30px; + } + } + } + } + } } > .search { @@ -409,7 +451,7 @@ export default defineComponent({ } > .emojis { - height: var(--height); + height: 100%; overflow-y: auto; overflow-x: hidden; diff --git a/packages/client/src/components/launch-pad.vue b/packages/client/src/components/launch-pad.vue index 8fe72c5f8c..9076cfb39f 100644 --- a/packages/client/src/components/launch-pad.vue +++ b/packages/client/src/components/launch-pad.vue @@ -1,5 +1,5 @@ <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="main"> <template v-for="item in items"> diff --git a/packages/client/src/components/post-form-dialog.vue b/packages/client/src/components/post-form-dialog.vue index b8b357bcde..dc4e842059 100644 --- a/packages/client/src/components/post-form-dialog.vue +++ b/packages/client/src/components/post-form-dialog.vue @@ -1,5 +1,5 @@ <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()"/> </MkModal> </template> diff --git a/packages/client/src/components/toast.vue b/packages/client/src/components/toast.vue index 740370003e..a04cf764ad 100644 --- a/packages/client/src/components/toast.vue +++ b/packages/client/src/components/toast.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-toast"> +<div class="mk-toast" style="{ zIndex }"> <transition name="notification-slide" appear @after-leave="$emit('closed')"> <XNotification v-if="showing" :notification="notification" class="notification _acrylic"/> </transition> @@ -9,6 +9,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; import XNotification from './notification.vue'; +import * as os from '@/os'; export default defineComponent({ components: { @@ -23,7 +24,8 @@ export default defineComponent({ emits: ['closed'], data() { return { - showing: true + showing: true, + zIndex: os.claimZIndex(true), }; }, mounted() { @@ -45,7 +47,6 @@ export default defineComponent({ .mk-toast { position: fixed; - z-index: 10000; left: 0; width: 250px; top: 32px; diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue index 687ce5e548..fde199bc6b 100644 --- a/packages/client/src/components/ui/menu.vue +++ b/packages/client/src/components/ui/menu.vue @@ -1,8 +1,8 @@ <template> <div ref="items" v-hotkey="keymap" class="rrevdjwt" - :class="{ center: align === 'center' }" - :style="{ width: width ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }" + :class="{ center: align === 'center', asDrawer }" + :style="{ width: (width && !asDrawer) ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }" @contextmenu.self="e => e.preventDefault()" > <template v-for="(item, i) in items2"> @@ -56,6 +56,10 @@ export default defineComponent({ type: Boolean, required: false }, + asDrawer: { + type: Boolean, + required: false + }, align: { type: String, requried: false @@ -279,5 +283,29 @@ export default defineComponent({ height: 1px; 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> diff --git a/packages/client/src/components/ui/modal-window.vue b/packages/client/src/components/ui/modal-window.vue index 61c2afe1ec..b4b8c2b965 100644 --- a/packages/client/src/components/ui/modal-window.vue +++ b/packages/client/src/components/ui/modal-window.vue @@ -1,5 +1,5 @@ <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="header"> <button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="fas fa-times"></i></button> diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue index 5fae1894e3..b875d01c07 100644 --- a/packages/client/src/components/ui/modal.vue +++ b/packages/client/src/components/ui/modal.vue @@ -1,17 +1,18 @@ <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"> - <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> - <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"> - <slot></slot> +<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" :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" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> + <div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> + <slot :max-height="maxHeight" :type="type"></slot> </div> </div> </transition> </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { defineComponent, nextTick, onMounted, computed, PropType, ref, watch } from 'vue'; import * as os from '@/os'; +import { isTouchUsing } from '@/scripts/touch'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === 'BODY') return null; @@ -27,6 +28,7 @@ export default defineComponent({ provide: { modal: true }, + props: { manualShowing: { type: Boolean, @@ -38,61 +40,86 @@ export default defineComponent({ required: false }, src: { + type: Object as PropType<HTMLElement>, required: false, + default: null, }, - position: { - required: false + preferType: { + required: false, + type: String, + default: 'auto', }, front: { type: Boolean, required: false, default: false, - } + }, + noOverlap: { + type: Boolean, + required: false, + default: true, + }, + transparentBg: { + type: Boolean, + required: false, + default: false, + }, }, + emits: ['opening', 'click', 'esc', 'close', 'closed'], - data() { - return { - zIndex: os.claimZIndex(this.front), - showing: true, - fixed: false, - transformOrigin: 'center', - contentClicking: false, + + 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 type = computed(() => { + if (props.preferType === 'auto') { + if (isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) { + return 'drawer'; + } else { + return props.src != null ? 'popup' : 'dialog'; + } + } else { + return props.preferType; + } + }); + + let contentClicking = false; + + const close = () => { + // eslint-disable-next-line vue/no-mutating-props + if (props.src) props.src.style.pointerEvents = 'auto'; + showing.value = false; + context.emit('close'); }; - }, - computed: { - keymap(): any { - return { - 'esc': () => this.$emit('esc'), - }; - }, - popup(): boolean { - return this.src != null; + + const onBgClick = () => { + if (contentClicking) return; + context.emit('click'); + }; + + if (type.value === 'drawer') { + maxHeight.value = window.innerHeight - 100; } - }, - mounted() { - this.$watch('src', () => { - this.fixed = getFixedContainer(this.src) != null; - this.$nextTick(() => { - this.align(); - }); - }, { immediate: true }); - this.$nextTick(() => { - const popover = this.$refs.content as any; - new ResizeObserver((entries, observer) => { - this.align(); - }).observe(popover); - }); - }, - methods: { - align() { - if (!this.popup) return; + const keymap = { + 'esc': () => context.emit('esc'), + }; - const popover = this.$refs.content as any; + const MARGIN = 16; + + const align = () => { + if (props.src == null) return; + if (type.value === 'drawer') return; + + const popover = content.value!; if (popover == null) return; - const rect = this.src.getBoundingClientRect(); + const rect = props.src.getBoundingClientRect(); const width = popover.offsetWidth; const height = popover.offsetHeight; @@ -100,102 +127,143 @@ export default defineComponent({ let left; let top; - if (this.srcCenter) { - const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); - const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2); + 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 + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); - const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight; + 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 (this.fixed) { + if (fixed.value) { + // 画面から横にはみ出る場合 if (left + width > window.innerWidth) { 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 { + 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) { - 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) { - top = 0; + top = MARGIN; } if (left < 0) { left = 0; } - if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) { - this.transformOrigin = 'center top'; + 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 { - this.transformOrigin = 'center'; + transformOrigin.value = 'center'; } popover.style.left = left + 'px'; popover.style.top = top + 'px'; - }, + }; - childRendered() { + const childRendered = () => { // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する - const content = this.$refs.content.children[0]; - content.addEventListener('mousedown', e => { - this.contentClicking = true; + const el = content.value!.children[0]; + el.addEventListener('mousedown', e => { + contentClicking = true; window.addEventListener('mouseup', e => { // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ setTimeout(() => { - this.contentClicking = false; + contentClicking = false; }, 100); }, { passive: true, once: true }); }, { passive: true }); - }, + }; - close() { - this.showing = false; - this.$emit('close'); - }, + onMounted(() => { + watch(() => props.src, async () => { + if (props.src) { + // eslint-disable-next-line vue/no-mutating-props + props.src.style.pointerEvents = 'none'; + } + fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); - onBgClick() { - if (this.contentClicking) return; - this.$emit('click'); - }, + await nextTick() + + align(); + }, { immediate: true, }); - onClosed() { - this.$emit('closed'); - } - } + 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> -<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> .modal-enter-active, .modal-leave-active { > .bg { - transition: opacity 0.3s !important; + transition: opacity 0.2s !important; } > .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 { @@ -206,17 +274,19 @@ export default defineComponent({ > .content { pointer-events: none; opacity: 0; + transform-origin: var(--transformOrigin); transform: scale(0.9); } } .modal-popup-enter-active, .modal-popup-leave-active { > .bg { - transition: opacity 0.3s !important; + transition: opacity 0.2s !important; } > .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 { @@ -227,48 +297,112 @@ export default defineComponent({ > .content { pointer-events: none; opacity: 0; + transform-origin: var(--transformOrigin); transform: scale(0.9); } } -.qzhlnise { - > .content:not(.popup) { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - margin: auto; - padding: 32px; - // TODO: mask-imageはiOSだとやたら重い。なんとかしたい - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%); - overflow: auto; - display: flex; +.modal-drawer-enter-active { + > .bg { + transition: opacity 0.2s !important; + } - @media (max-width: 500px) { - padding: 16px; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); + > .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 { + > .bg { + &.transparent { + background: transparent; + -webkit-backdrop-filter: none; + backdrop-filter: none; } + } - > ::v-deep(*) { + &.dialog { + > .content { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; margin: auto; - } + padding: 32px; + // TODO: mask-imageはiOSだとやたら重い。なんとかしたい + -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%); + mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%); + overflow: auto; + display: flex; + + @media (max-width: 500px) { + padding: 16px; + -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); + mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); + } - &.top { > ::v-deep(*) { - margin-top: 0; + margin: auto; + } + + &.top { + > ::v-deep(*) { + margin-top: 0; + } } } } - > .content.popup { - position: absolute; + &.popup { + > .content { + position: absolute; - &.fixed { + &.fixed { + position: fixed; + } + } + } + + &.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> diff --git a/packages/client/src/components/ui/popup-menu.vue b/packages/client/src/components/ui/popup-menu.vue index 93bafddaee..f1eedcc622 100644 --- a/packages/client/src/components/ui/popup-menu.vue +++ b/packages/client/src/components/ui/popup-menu.vue @@ -1,17 +1,17 @@ <template> -<MkPopup ref="popup" v-slot="{ maxHeight, close }" :src="src" @closed="$emit('closed')"> - <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" class="_popup _shadow" @close="close()"/> -</MkPopup> +<MkModal ref="modal" v-slot="{ type, maxHeight }" :src="src" :transparent-bg="true" @click="$refs.modal.close()" @closed="$emit('closed')"> + <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()"/> +</MkModal> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import MkPopup from './popup.vue'; +import MkModal from './modal.vue'; import MkMenu from './menu.vue'; export default defineComponent({ components: { - MkPopup, + MkModal, MkMenu, }, @@ -40,3 +40,13 @@ export default defineComponent({ emits: ['close', 'closed'], }); </script> + +<style lang="scss" scoped> +.sfhdhdhq { + &.drawer { + border-radius: 24px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } +} +</style> diff --git a/packages/client/src/components/ui/popup.vue b/packages/client/src/components/ui/popup.vue deleted file mode 100644 index abacd828ae..0000000000 --- a/packages/client/src/components/ui/popup.vue +++ /dev/null @@ -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> diff --git a/packages/client/src/components/url-preview-popup.vue b/packages/client/src/components/url-preview-popup.vue index 65076c6dda..75c5bca7dd 100644 --- a/packages/client/src/components/url-preview-popup.vue +++ b/packages/client/src/components/url-preview-popup.vue @@ -1,5 +1,5 @@ <template> -<div class="fgmtyycl" :style="{ top: top + 'px', left: left + 'px' }"> +<div class="fgmtyycl" :style="{ zIndex, top: top + 'px', left: left + 'px' }"> <transition name="zoom" @after-leave="$emit('closed')"> <MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/> </transition> @@ -35,6 +35,7 @@ export default defineComponent({ u: null, top: 0, left: 0, + zIndex: os.claimZIndex(), }; }, @@ -52,7 +53,6 @@ export default defineComponent({ <style lang="scss" scoped> .fgmtyycl { position: absolute; - z-index: 11000; width: 500px; max-width: calc(90vw - 12px); pointer-events: none; diff --git a/packages/client/src/components/user-preview.vue b/packages/client/src/components/user-preview.vue index 5289ce54d7..9b3f15b61f 100644 --- a/packages/client/src/components/user-preview.vue +++ b/packages/client/src/components/user-preview.vue @@ -1,6 +1,6 @@ <template> <transition name="popup" appear @after-leave="$emit('closed')"> - <div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }"> + <div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }"> <div v-if="fetched" class="info"> <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> @@ -65,6 +65,7 @@ export default defineComponent({ fetched: false, top: 0, left: 0, + zIndex: os.claimZIndex(), }; }, @@ -109,7 +110,6 @@ export default defineComponent({ .fxxzrfni { position: absolute; - z-index: 11000; width: 300px; overflow: hidden; transform-origin: center top; |