diff options
Diffstat (limited to 'src/client/components')
| -rw-r--r-- | src/client/components/drive.file.vue | 2 | ||||
| -rw-r--r-- | src/client/components/drive.vue | 2 | ||||
| -rw-r--r-- | src/client/components/emoji-picker-dialog.vue | 14 | ||||
| -rw-r--r-- | src/client/components/note-detailed.vue | 8 | ||||
| -rw-r--r-- | src/client/components/note.vue | 8 | ||||
| -rw-r--r-- | src/client/components/post-form-attaches.vue | 2 | ||||
| -rw-r--r-- | src/client/components/post-form.vue | 2 | ||||
| -rw-r--r-- | src/client/components/sample.vue | 2 | ||||
| -rw-r--r-- | src/client/components/ui/button.vue | 2 | ||||
| -rw-r--r-- | src/client/components/ui/input.vue | 6 | ||||
| -rw-r--r-- | src/client/components/ui/popup-menu.vue (renamed from src/client/components/ui/modal-menu.vue) | 29 | ||||
| -rw-r--r-- | src/client/components/ui/popup.vue | 208 | ||||
| -rw-r--r-- | src/client/components/ui/select.vue | 6 | ||||
| -rw-r--r-- | src/client/components/ui/textarea.vue | 6 |
14 files changed, 250 insertions, 47 deletions
diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue index 3d20de23e9..b1be3d0cab 100644 --- a/src/client/components/drive.file.vue +++ b/src/client/components/drive.file.vue @@ -114,7 +114,7 @@ export default defineComponent({ if (this.selectMode) { this.$emit('chosen', this.file); } else { - os.modalMenu(this.getMenu(), ev.currentTarget || ev.target); + os.popupMenu(this.getMenu(), ev.currentTarget || ev.target); } }, diff --git a/src/client/components/drive.vue b/src/client/components/drive.vue index 98f7b54828..5dadf9a11f 100644 --- a/src/client/components/drive.vue +++ b/src/client/components/drive.vue @@ -629,7 +629,7 @@ export default defineComponent({ }, showMenu(ev) { - os.modalMenu(this.getMenu(), ev.currentTarget || ev.target); + os.popupMenu(this.getMenu(), ev.currentTarget || ev.target); }, onContextmenu(ev) { diff --git a/src/client/components/emoji-picker-dialog.vue b/src/client/components/emoji-picker-dialog.vue index 9400819a1f..5860acaa42 100644 --- a/src/client/components/emoji-picker-dialog.vue +++ b/src/client/components/emoji-picker-dialog.vue @@ -1,17 +1,17 @@ <template> -<MkModal ref="modal" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')"> - <MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen" ref="picker"/> -</MkModal> +<MkPopup ref="popup" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.popup.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')"> + <MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen" ref="picker" style="box-shadow: 0 8px 32px rgb(0 0 0 / 30%);"/> +</MkPopup> </template> <script lang="ts"> import { defineComponent, markRaw } from 'vue'; -import MkModal from '@client/components/ui/modal.vue'; +import MkPopup from '@client/components/ui/popup.vue'; import MkEmojiPicker from '@client/components/emoji-picker.vue'; export default defineComponent({ components: { - MkModal, + MkPopup, MkEmojiPicker, }, @@ -33,7 +33,7 @@ export default defineComponent({ }, }, - emits: ['done', 'closed'], + emits: ['done', 'close', 'closed'], data() { return { @@ -44,7 +44,7 @@ export default defineComponent({ methods: { chosen(emoji: any) { this.$emit('done', emoji); - this.$refs.modal.close(); + this.$refs.popup.close(); }, opening() { diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue index 6040ad378f..d601052927 100644 --- a/src/client/components/note-detailed.vue +++ b/src/client/components/note-detailed.vue @@ -454,7 +454,7 @@ export default defineComponent({ renote(viaKeyboard = false) { pleaseLogin(); this.blur(); - os.modalMenu([{ + os.popupMenu([{ text: this.$ts.renote, icon: 'fas fa-retweet', action: () => { @@ -743,14 +743,14 @@ export default defineComponent({ }, menu(viaKeyboard = false) { - os.modalMenu(this.getMenu(), this.$refs.menuButton, { + os.popupMenu(this.getMenu(), this.$refs.menuButton, { viaKeyboard }).then(this.focus); }, showRenoteMenu(viaKeyboard = false) { if (!this.isMyRenote) return; - os.modalMenu([{ + os.popupMenu([{ text: this.$ts.unrenote, icon: 'fas fa-trash-alt', danger: true, @@ -794,7 +794,7 @@ export default defineComponent({ async clip() { const clips = await os.api('clips/list'); - os.modalMenu([{ + os.popupMenu([{ icon: 'fas fa-plus', text: this.$ts.createNew, action: async () => { diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 504d07c0eb..873b96030a 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -429,7 +429,7 @@ export default defineComponent({ renote(viaKeyboard = false) { pleaseLogin(); this.blur(); - os.modalMenu([{ + os.popupMenu([{ text: this.$ts.renote, icon: 'fas fa-retweet', action: () => { @@ -718,14 +718,14 @@ export default defineComponent({ }, menu(viaKeyboard = false) { - os.modalMenu(this.getMenu(), this.$refs.menuButton, { + os.popupMenu(this.getMenu(), this.$refs.menuButton, { viaKeyboard }).then(this.focus); }, showRenoteMenu(viaKeyboard = false) { if (!this.isMyRenote) return; - os.modalMenu([{ + os.popupMenu([{ text: this.$ts.unrenote, icon: 'fas fa-trash-alt', danger: true, @@ -769,7 +769,7 @@ export default defineComponent({ async clip() { const clips = await os.api('clips/list'); - os.modalMenu([{ + os.popupMenu([{ icon: 'fas fa-plus', text: this.$ts.createNew, action: async () => { diff --git a/src/client/components/post-form-attaches.vue b/src/client/components/post-form-attaches.vue index 27e20fdfa8..9365365653 100644 --- a/src/client/components/post-form-attaches.vue +++ b/src/client/components/post-form-attaches.vue @@ -112,7 +112,7 @@ export default defineComponent({ showFileMenu(file, ev: MouseEvent) { if (this.menu) return; - this.menu = os.modalMenu([{ + this.menu = os.popupMenu([{ text: this.$ts.renameFile, icon: 'fas fa-i-cursor', action: () => { this.rename(file) } diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index ed2a934c26..969f8563a4 100644 --- a/src/client/components/post-form.vue +++ b/src/client/components/post-form.vue @@ -664,7 +664,7 @@ export default defineComponent({ }, showActions(ev) { - os.modalMenu(postFormActions.map(action => ({ + os.popupMenu(postFormActions.map(action => ({ text: action.title, action: () => { action.handler({ diff --git a/src/client/components/sample.vue b/src/client/components/sample.vue index 53eac0e2e6..bce02466f6 100644 --- a/src/client/components/sample.vue +++ b/src/client/components/sample.vue @@ -93,7 +93,7 @@ export default defineComponent({ }, async openMenu(ev) { - os.modalMenu([{ + os.popupMenu([{ type: 'label', text: 'Fruits' }, { diff --git a/src/client/components/ui/button.vue b/src/client/components/ui/button.vue index 8a83943825..000f6302a0 100644 --- a/src/client/components/ui/button.vue +++ b/src/client/components/ui/button.vue @@ -115,7 +115,7 @@ export default defineComponent({ z-index: 1; // 他コンポーネントのbox-shadowに隠されないようにするため display: block; min-width: 100px; - width: min-content; + width: max-content; padding: 8px 14px; text-align: center; font-weight: normal; diff --git a/src/client/components/ui/input.vue b/src/client/components/ui/input.vue index 3e2a5fb0dd..05ce5d3e15 100644 --- a/src/client/components/ui/input.vue +++ b/src/client/components/ui/input.vue @@ -210,8 +210,7 @@ export default defineComponent({ > .label { font-size: 0.85em; - padding: 0 0 6px 6px; - font-weight: bold; + padding: 0 0 8px 12px; user-select: none; &:empty { @@ -221,7 +220,7 @@ export default defineComponent({ > .caption { font-size: 0.8em; - padding: 6px 0 0 6px; + padding: 8px 0 0 12px; color: var(--fgTransparentWeak); &:empty { @@ -251,6 +250,7 @@ export default defineComponent({ outline: none; box-shadow: none; box-sizing: border-box; + transition: border-color 0.1s ease-out; &:hover { border-color: var(--inputBorderHover); diff --git a/src/client/components/ui/modal-menu.vue b/src/client/components/ui/popup-menu.vue index aac4be9c3b..ceb3c47bf9 100644 --- a/src/client/components/ui/modal-menu.vue +++ b/src/client/components/ui/popup-menu.vue @@ -1,20 +1,25 @@ <template> -<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')"> - <MkMenu :items="items" :align="align" @close="$refs.modal.close()" class="_popup"/> -</MkModal> +<MkPopup ref="popup" :src="src" @closed="$emit('closed')"> + <MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup" style="box-shadow: 0 8px 32px rgb(0 0 0 / 30%);"/> +</MkPopup> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import MkModal from './modal.vue'; +import MkPopup from './popup.vue'; import MkMenu from './menu.vue'; export default defineComponent({ components: { - MkModal, + MkPopup, MkMenu, }, + props: { + showing: { + type: Boolean, + required: true, + }, items: { type: Array, required: true @@ -31,17 +36,7 @@ export default defineComponent({ required: false }, }, - emits: ['closed'], - computed: { - keymap(): any { - return { - 'esc': () => this.$refs.modal.close(), - }; - }, - }, + + emits: ['close', 'closed'], }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/components/ui/popup.vue b/src/client/components/ui/popup.vue new file mode 100644 index 0000000000..85df3f432d --- /dev/null +++ b/src/client/components/ui/popup.vue @@ -0,0 +1,208 @@ +<template> +<transition :name="$store.state.animation ? 'popup-menu' : ''" :duration="$store.state.animation ? 300 : 0" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered"> + <div v-show="manualShowing != null ? manualShowing : showing" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" ref="content" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> + <slot></slot> + </div> +</transition> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; + +function getFixedContainer(el: Element | null): 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: { + required: false, + }, + position: { + required: false + }, + front: { + type: Boolean, + required: false, + default: false, + } + }, + + emits: ['opening', 'click', 'esc', 'close', 'closed'], + + data() { + return { + showing: true, + fixed: false, + transformOrigin: 'center', + contentClicking: false, + }; + }, + + 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); + }); + + document.addEventListener('mousedown', this.onDocumentClick); + }, + + beforeUnmount() { + document.removeEventListener('mousedown', this.onDocumentClick); + }, + + methods: { + align() { + if (this.src == null) return; + + const popover = this.$refs.content as any; + + if (popover == null) return; + + const rect = this.src.getBoundingClientRect(); + + const width = popover.offsetWidth; + const height = popover.offsetHeight; + + 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); + 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; + left = (x - (width / 2)); + top = y; + } + + if (this.fixed) { + if (left + width > window.innerWidth) { + left = window.innerWidth - width; + } + + if (top + height > window.innerHeight) { + top = window.innerHeight - 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 < 0) { + top = 0; + } + + if (left < 0) { + left = 0; + } + + if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) { + this.transformOrigin = 'center top'; + } else { + this.transformOrigin = 'center'; + } + + popover.style.left = left + 'px'; + popover.style.top = top + 'px'; + }, + + childRendered() { + // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する + const content = this.$refs.content.children[0]; + content.addEventListener('mousedown', e => { + this.contentClicking = true; + window.addEventListener('mouseup', e => { + // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ + setTimeout(() => { + this.contentClicking = false; + }, 100); + }, { passive: true, once: true }); + }, { passive: true }); + }, + + close() { + this.showing = false; + this.$emit('close'); + }, + + onClosed() { + this.$emit('closed'); + }, + + onDocumentClick(ev) { + const flyoutElement = this.$refs.content; + let targetElement = ev.target; + do { + if (targetElement === flyoutElement) { + return; + } + targetElement = targetElement.parentNode; + } while (targetElement); + this.close(); + } + } +}); +</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; + z-index: 10000; + + &.fixed { + position: fixed; + } + + &.front { + z-index: 20000; + } +} +</style> diff --git a/src/client/components/ui/select.vue b/src/client/components/ui/select.vue index 987d4f194c..e9d43d8a64 100644 --- a/src/client/components/ui/select.vue +++ b/src/client/components/ui/select.vue @@ -155,8 +155,7 @@ export default defineComponent({ > .label { font-size: 0.85em; - padding: 0 0 6px 6px; - font-weight: bold; + padding: 0 0 8px 12px; user-select: none; &:empty { @@ -166,7 +165,7 @@ export default defineComponent({ > .caption { font-size: 0.8em; - padding: 6px 0 0 6px; + padding: 8px 0 0 12px; color: var(--fgTransparentWeak); &:empty { @@ -197,6 +196,7 @@ export default defineComponent({ box-shadow: none; box-sizing: border-box; cursor: pointer; + transition: border-color 0.1s ease-out; &:hover { border-color: var(--inputBorderHover); diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue index a61324f25f..53a141f011 100644 --- a/src/client/components/ui/textarea.vue +++ b/src/client/components/ui/textarea.vue @@ -176,8 +176,7 @@ export default defineComponent({ > .label { font-size: 0.85em; - padding: 0 0 6px 6px; - font-weight: bold; + padding: 0 0 8px 12px; user-select: none; &:empty { @@ -187,7 +186,7 @@ export default defineComponent({ > .caption { font-size: 0.8em; - padding: 6px 0 0 6px; + padding: 8px 0 0 12px; color: var(--fgTransparentWeak); &:empty { @@ -218,6 +217,7 @@ export default defineComponent({ outline: none; box-shadow: none; box-sizing: border-box; + transition: border-color 0.1s ease-out; &:hover { border-color: var(--inputBorderHover); |