summaryrefslogtreecommitdiff
path: root/src/client/components/ui/modal.vue
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-10-17 20:12:00 +0900
committerGitHub <noreply@github.com>2020-10-17 20:12:00 +0900
commit7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a (patch)
tree2263a06acec7fa21882366bae26d1a983ce21135 /src/client/components/ui/modal.vue
parentCW の input でも投稿ショートカットが動作するように (#6690) (diff)
downloadmisskey-7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a.tar.gz
misskey-7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a.tar.bz2
misskey-7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a.zip
Migrate to Vue3 (#6587)
* Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
Diffstat (limited to 'src/client/components/ui/modal.vue')
-rw-r--r--src/client/components/ui/modal.vue232
1 files changed, 232 insertions, 0 deletions
diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue
new file mode 100644
index 0000000000..4cc96bb8da
--- /dev/null
+++ b/src/client/components/ui/modal.vue
@@ -0,0 +1,232 @@
+<template>
+<div class="mk-modal" v-hotkey.global="keymap" :style="{ pointerEvents: showing ? 'auto' : 'none' }">
+ <transition :name="$store.state.device.animation ? 'modal-bg' : ''" appear>
+ <div class="bg _modalBg" v-if="showing" @click="onBgClick"></div>
+ </transition>
+ <div class="content" :class="{ popup, fixed, top: position === 'top' }" @click.self="onBgClick" ref="content">
+ <transition :name="$store.state.device.animation ? popup ? 'modal-popup-content' : 'modal-content' : ''" appear @after-leave="$emit('closed')" @after-enter="childRendered">
+ <slot v-if="showing"></slot>
+ </transition>
+ </div>
+</div>
+</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({
+ provide: {
+ modal: true
+ },
+ props: {
+ srcCenter: {
+ type: Boolean,
+ required: false
+ },
+ src: {
+ required: false,
+ },
+ position: {
+ required: false
+ }
+ },
+ emits: ['click', 'esc', 'closed'],
+ data() {
+ return {
+ showing: true,
+ fixed: false,
+ transformOrigin: 'center',
+ contentClicking: false,
+ };
+ },
+ computed: {
+ keymap(): any {
+ return {
+ 'esc': () => this.$emit('esc'),
+ };
+ },
+ popup(): boolean {
+ return this.src != null;
+ }
+ },
+ mounted() {
+ this.fixed = getFixedContainer(this.src) != null;
+
+ this.$nextTick(() => {
+ if (!this.popup) return;
+
+ const popover = this.$refs.content as any;
+
+ // TODO: ResizeObserver無くしたい
+ new ResizeObserver((entries, observer) => {
+ 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;
+ }
+
+ if (top + height - window.pageYOffset > window.innerHeight) {
+ top = window.innerHeight - height + window.pageYOffset;
+ }
+ }
+
+ if (top < 0) {
+ top = 0;
+ }
+
+ if (left < 0) {
+ left = 0;
+ }
+
+ if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) {
+ this.transformOrigin = 'center top';
+ }
+
+ popover.style.left = left + 'px';
+ popover.style.top = top + 'px';
+ }).observe(popover);
+ });
+ },
+ methods: {
+ 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;
+ },
+
+ onBgClick() {
+ if (this.contentClicking) return;
+ this.$emit('click');
+ }
+ }
+});
+</script>
+
+<style vars="{ transformOrigin }">
+.modal-popup-content-enter-active, .modal-popup-content-leave-active,
+.modal-content-enter-from, .modal-content-leave-to {
+ transform-origin: var(--transformOrigin);
+}
+</style>
+
+<style lang="scss" scoped>
+.modal-bg-enter-active, .modal-bg-leave-active {
+ transition: opacity 0.3s !important;
+}
+.modal-bg-enter-from, .modal-bg-leave-to {
+ opacity: 0;
+}
+
+.modal-content-enter-active, .modal-content-leave-active {
+ transition: opacity 0.3s, transform 0.3s !important;
+}
+.modal-content-enter-from, .modal-content-leave-to {
+ pointer-events: none;
+ opacity: 0;
+ transform: scale(0.9);
+}
+
+.modal-popup-content-enter-active, .modal-popup-content-leave-active {
+ transition: opacity 0.3s, transform 0.3s !important;
+}
+.modal-popup-content-enter-from, .modal-popup-content-leave-to {
+ pointer-events: none;
+ opacity: 0;
+ transform: scale(0.9);
+}
+
+.mk-modal {
+ > .bg {
+ z-index: 10000;
+ }
+
+ > .content:not(.popup) {
+ position: fixed;
+ z-index: 10000;
+ 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%);
+ }
+
+ > * {
+ margin: auto;
+ }
+
+ &.top {
+ > * {
+ margin-top: 0;
+ }
+ }
+ }
+
+ > .content.popup {
+ position: absolute;
+ z-index: 10000;
+
+ &.fixed {
+ position: fixed;
+ }
+ }
+}
+</style>