From 06e817af597e7ee621bdeb370c72d7555482d943 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 27 Feb 2021 11:18:53 +0900
Subject: perf: use overflow: clip instead of hidden

---
 src/client/components/note.vue | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

(limited to 'src/client/components/note.vue')

diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 24c374869d..377e5991b2 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -867,7 +867,8 @@ export default defineComponent({
 .tkcbzcuz {
 	position: relative;
 	transition: box-shadow 0.1s ease;
-	overflow: hidden;
+	overflow: hidden; // overflow: clip; をSafariが対応したら消す
+	overflow: clip;
 	contain: content;
 
 	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
@@ -954,7 +955,8 @@ export default defineComponent({
 		}
 
 		> span {
-			overflow: hidden;
+			overflow: hidden; // overflow: clip; をSafariが対応したら消す
+			overflow: clip;
 			flex-shrink: 1;
 			text-overflow: ellipsis;
 			white-space: nowrap;
@@ -1026,7 +1028,8 @@ export default defineComponent({
 					&.collapsed {
 						position: relative;
 						max-height: 9em;
-						overflow: hidden;
+						overflow: hidden; // overflow: clip; をSafariが対応したら消す
+						overflow: clip;
 
 						> .fade {
 							display: block;
-- 
cgit v1.3.1-freya


From f29d417b30be5479a04a1898b883d9fa1cf11fe9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 27 Feb 2021 13:08:34 +0900
Subject: 絵文字ピッカーを常に表示するように
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Resolve #7265
---
 src/client/components/emoji-picker-dialog.vue      | 191 ++++++++++++++++++++
 src/client/components/emoji-picker-window.vue      | 197 +++++++++++++++++++++
 src/client/components/emoji-picker.vue             | 155 ++++++++--------
 src/client/components/note-detailed.vue            |   2 +-
 src/client/components/note.vue                     |   2 +-
 src/client/components/post-form.vue                |   4 +-
 src/client/components/ui/window.vue                |  33 +++-
 src/client/os.ts                                   |  62 ++++++-
 src/client/pages/messaging/messaging-room.form.vue |   4 +-
 src/client/pages/settings/reaction.vue             |   2 +-
 src/client/style.scss                              |   4 +-
 src/client/ui/chat/note.vue                        |   2 +-
 src/client/ui/chat/post-form.vue                   |   4 +-
 13 files changed, 556 insertions(+), 106 deletions(-)
 create mode 100644 src/client/components/emoji-picker-dialog.vue
 create mode 100644 src/client/components/emoji-picker-window.vue

(limited to 'src/client/components/note.vue')

diff --git a/src/client/components/emoji-picker-dialog.vue b/src/client/components/emoji-picker-dialog.vue
new file mode 100644
index 0000000000..177b5db44d
--- /dev/null
+++ b/src/client/components/emoji-picker-dialog.vue
@@ -0,0 +1,191 @@
+<template>
+<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
+	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import MkModal from '@/components/ui/modal.vue';
+import MkEmojiPicker from '@/components/emoji-picker.vue';
+
+export default defineComponent({
+	components: {
+		MkModal,
+		MkEmojiPicker,
+	},
+
+	props: {
+		src: {
+			required: false
+		},
+		showPinned: {
+			required: false,
+			default: true
+		},
+		asReactionPicker: {
+			required: false
+		},
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+
+		};
+	},
+
+	methods: {
+		chosen(emoji: any) {
+			this.$emit('done', emoji);
+			this.$refs.modal.close();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.omfetrab {
+	$pad: 8px;
+	--eachSize: 40px;
+
+	display: flex;
+	flex-direction: column;
+	contain: content;
+
+	&.big {
+		--eachSize: 44px;
+	}
+
+	&.w1 {
+		width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
+	}
+
+	&.w2 {
+		width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
+	}
+
+	&.w3 {
+		width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
+	}
+
+	&.h1 {
+		--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
+	}
+
+	&.h2 {
+		--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
+	}
+
+	&.h3 {
+		--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
+	}
+
+	> .search {
+		width: 100%;
+		padding: 12px;
+		box-sizing: border-box;
+		font-size: 1em;
+		outline: none;
+		border: none;
+		background: transparent;
+		color: var(--fg);
+
+		&:not(.filled) {
+			order: 1;
+			z-index: 2;
+			box-shadow: 0px -1px 0 0px var(--divider);
+		}
+	}
+
+	> .emojis {
+		height: var(--height);
+		overflow-y: auto;
+		overflow-x: hidden;
+
+		scrollbar-width: none;
+
+		&::-webkit-scrollbar {
+			display: none;
+		}
+
+		> .index {
+			min-height: var(--height);
+			position: relative;
+			border-bottom: solid 1px var(--divider);
+				
+			> .arrow {
+				position: absolute;
+				bottom: 0;
+				left: 0;
+				width: 100%;
+				padding: 16px 0;
+				text-align: center;
+				opacity: 0.5;
+				pointer-events: none;
+			}
+		}
+
+		section {
+			> header {
+				position: sticky;
+				top: 0;
+				left: 0;
+				z-index: 1;
+				padding: 8px;
+				font-size: 12px;
+			}
+
+			> div {
+				padding: $pad;
+
+				> button {
+					position: relative;
+					padding: 0;
+					width: var(--eachSize);
+					height: var(--eachSize);
+					border-radius: 4px;
+
+					&:focus {
+						outline: solid 2px var(--focus);
+						z-index: 1;
+					}
+
+					&:hover {
+						background: rgba(0, 0, 0, 0.05);
+					}
+
+					&:active {
+						background: var(--accent);
+						box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
+					}
+
+					> * {
+						font-size: 24px;
+						height: 1.25em;
+						vertical-align: -.25em;
+						pointer-events: none;
+					}
+				}
+			}
+
+			&.result {
+				border-bottom: solid 1px var(--divider);
+
+				&:empty {
+					display: none;
+				}
+			}
+
+			&.unicode {
+				min-height: 384px;
+			}
+
+			&.custom {
+				min-height: 64px;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/emoji-picker-window.vue b/src/client/components/emoji-picker-window.vue
new file mode 100644
index 0000000000..fb6a5a9072
--- /dev/null
+++ b/src/client/components/emoji-picker-window.vue
@@ -0,0 +1,197 @@
+<template>
+<MkWindow ref="window"
+	:initial-width="null"
+	:initial-height="null"
+	:can-resize="false"
+	:mini="true"
+	:front="true"
+	@closed="$emit('closed')"
+>
+	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
+</MkWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import MkWindow from '@/components/ui/window.vue';
+import MkEmojiPicker from '@/components/emoji-picker.vue';
+
+export default defineComponent({
+	components: {
+		MkWindow,
+		MkEmojiPicker,
+	},
+
+	props: {
+		src: {
+			required: false
+		},
+		showPinned: {
+			required: false,
+			default: true
+		},
+		asReactionPicker: {
+			required: false
+		},
+	},
+
+	emits: ['chosen', 'closed'],
+
+	data() {
+		return {
+
+		};
+	},
+
+	methods: {
+		chosen(emoji: any) {
+			this.$emit('chosen', emoji);
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.omfetrab {
+	$pad: 8px;
+	--eachSize: 40px;
+
+	display: flex;
+	flex-direction: column;
+	contain: content;
+
+	&.big {
+		--eachSize: 44px;
+	}
+
+	&.w1 {
+		width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
+	}
+
+	&.w2 {
+		width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
+	}
+
+	&.w3 {
+		width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
+	}
+
+	&.h1 {
+		--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
+	}
+
+	&.h2 {
+		--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
+	}
+
+	&.h3 {
+		--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
+	}
+
+	> .search {
+		width: 100%;
+		padding: 12px;
+		box-sizing: border-box;
+		font-size: 1em;
+		outline: none;
+		border: none;
+		background: transparent;
+		color: var(--fg);
+
+		&:not(.filled) {
+			order: 1;
+			z-index: 2;
+			box-shadow: 0px -1px 0 0px var(--divider);
+		}
+	}
+
+	> .emojis {
+		height: var(--height);
+		overflow-y: auto;
+		overflow-x: hidden;
+
+		scrollbar-width: none;
+
+		&::-webkit-scrollbar {
+			display: none;
+		}
+
+		> .index {
+			min-height: var(--height);
+			position: relative;
+			border-bottom: solid 1px var(--divider);
+				
+			> .arrow {
+				position: absolute;
+				bottom: 0;
+				left: 0;
+				width: 100%;
+				padding: 16px 0;
+				text-align: center;
+				opacity: 0.5;
+				pointer-events: none;
+			}
+		}
+
+		section {
+			> header {
+				position: sticky;
+				top: 0;
+				left: 0;
+				z-index: 1;
+				padding: 8px;
+				font-size: 12px;
+			}
+
+			> div {
+				padding: $pad;
+
+				> button {
+					position: relative;
+					padding: 0;
+					width: var(--eachSize);
+					height: var(--eachSize);
+					border-radius: 4px;
+
+					&:focus {
+						outline: solid 2px var(--focus);
+						z-index: 1;
+					}
+
+					&:hover {
+						background: rgba(0, 0, 0, 0.05);
+					}
+
+					&:active {
+						background: var(--accent);
+						box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
+					}
+
+					> * {
+						font-size: 24px;
+						height: 1.25em;
+						vertical-align: -.25em;
+						pointer-events: none;
+					}
+				}
+			}
+
+			&.result {
+				border-bottom: solid 1px var(--divider);
+
+				&:empty {
+					display: none;
+				}
+			}
+
+			&.unicode {
+				min-height: 384px;
+			}
+
+			&.custom {
+				min-height: 64px;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
index 9a261ef83f..93530e16c8 100644
--- a/src/client/components/emoji-picker.vue
+++ b/src/client/components/emoji-picker.vue
@@ -1,93 +1,91 @@
 <template>
-<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
-	<div class="omfetrab _popup" :class="['w' + width, 'h' + height, { big }]">
-		<input ref="search" class="search" :class="{ filled: q != null && q != '' }" v-model.trim="q" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
-		<div class="emojis" ref="emojis">
-			<section class="result">
-				<div v-if="searchResultCustom.length > 0">
-					<button v-for="emoji in searchResultCustom"
-						class="_button"
-						:title="emoji.name"
-						@click="chosen(emoji, $event)"
-						:key="emoji"
-						tabindex="0"
-					>
-						<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
-						<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
-					</button>
-				</div>
-				<div v-if="searchResultUnicode.length > 0">
-					<button v-for="emoji in searchResultUnicode"
-						class="_button"
-						:title="emoji.name"
-						@click="chosen(emoji, $event)"
-						:key="emoji.name"
-						tabindex="0"
-					>
-						<MkEmoji :emoji="emoji.char"/>
-					</button>
-				</div>
-			</section>
-
-			<div class="index">
-				<section v-if="showPinned">
-					<div>
-						<button v-for="emoji in pinned"
-							class="_button"
-							@click="chosen(emoji, $event)"
-							tabindex="0"
-						>
-							<MkEmoji :emoji="emoji" :normal="true"/>
-						</button>
-					</div>
-				</section>
-
-				<section>
-					<header class="_acrylic"><Fa :icon="faClock" fixed-width/> {{ $ts.recentUsed }}</header>
-					<div>
-						<button v-for="emoji in $store.state.recentlyUsedEmojis"
-							class="_button"
-							@click="chosen(emoji, $event)"
-							:key="emoji"
-						>
-							<MkEmoji :emoji="emoji" :normal="true"/>
-						</button>
-					</div>
-				</section>
-
-				<div class="arrow"><Fa :icon="faChevronDown"/></div>
+<div class="omfetrab _popup" :class="['w' + width, 'h' + height, { big }]">
+	<input ref="search" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" v-model.trim="q" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
+	<div class="emojis" ref="emojis">
+		<section class="result">
+			<div v-if="searchResultCustom.length > 0">
+				<button v-for="emoji in searchResultCustom"
+					class="_button"
+					:title="emoji.name"
+					@click="chosen(emoji, $event)"
+					:key="emoji"
+					tabindex="0"
+				>
+					<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
+					<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+				</button>
 			</div>
+			<div v-if="searchResultUnicode.length > 0">
+				<button v-for="emoji in searchResultUnicode"
+					class="_button"
+					:title="emoji.name"
+					@click="chosen(emoji, $event)"
+					:key="emoji.name"
+					tabindex="0"
+				>
+					<MkEmoji :emoji="emoji.char"/>
+				</button>
+			</div>
+		</section>
 
-			<section v-for="category in customEmojiCategories" :key="'custom:' + category" class="custom">
-				<header class="_acrylic" v-appear="() => visibleCategories[category] = true">{{ category || $ts.other }}</header>
-				<div v-if="visibleCategories[category]">
-					<button v-for="emoji in customEmojis.filter(e => e.category === category)"
+		<div class="index">
+			<section v-if="showPinned">
+				<div>
+					<button v-for="emoji in pinned"
 						class="_button"
-						:title="emoji.name"
 						@click="chosen(emoji, $event)"
-						:key="emoji.name"
+						tabindex="0"
 					>
-						<img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+						<MkEmoji :emoji="emoji" :normal="true"/>
 					</button>
 				</div>
 			</section>
 
-			<section v-for="category in categories" :key="category.name" class="unicode">
-				<header class="_acrylic" v-appear="() => category.isActive = true"><Fa :icon="category.icon" fixed-width/> {{ category.name }}</header>
-				<div v-if="category.isActive">
-					<button v-for="emoji in emojilist.filter(e => e.category === category.name)"
+			<section>
+				<header class="_acrylic"><Fa :icon="faClock" fixed-width/> {{ $ts.recentUsed }}</header>
+				<div>
+					<button v-for="emoji in $store.state.recentlyUsedEmojis"
 						class="_button"
-						:title="emoji.name"
 						@click="chosen(emoji, $event)"
-						:key="emoji.name"
+						:key="emoji"
 					>
-						<MkEmoji :emoji="emoji.char"/>
+						<MkEmoji :emoji="emoji" :normal="true"/>
 					</button>
 				</div>
 			</section>
+
+			<div class="arrow"><Fa :icon="faChevronDown"/></div>
 		</div>
+
+		<section v-for="category in customEmojiCategories" :key="'custom:' + category" class="custom">
+			<header class="_acrylic" v-appear="() => visibleCategories[category] = true">{{ category || $ts.other }}</header>
+			<div v-if="visibleCategories[category]">
+				<button v-for="emoji in customEmojis.filter(e => e.category === category)"
+					class="_button"
+					:title="emoji.name"
+					@click="chosen(emoji, $event)"
+					:key="emoji.name"
+				>
+					<img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+				</button>
+			</div>
+		</section>
+
+		<section v-for="category in categories" :key="category.name" class="unicode">
+			<header class="_acrylic" v-appear="() => category.isActive = true"><Fa :icon="category.icon" fixed-width/> {{ category.name }}</header>
+			<div v-if="category.isActive">
+				<button v-for="emoji in emojilist.filter(e => e.category === category.name)"
+					class="_button"
+					:title="emoji.name"
+					@click="chosen(emoji, $event)"
+					:key="emoji.name"
+				>
+					<MkEmoji :emoji="emoji.char"/>
+				</button>
+			</div>
+		</section>
 	</div>
-</MkModal>
+</div>
 </template>
 
 <script lang="ts">
@@ -96,7 +94,6 @@ import { emojilist } from '../../misc/emojilist';
 import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faClock, faUser, faChevronDown } from '@fortawesome/free-solid-svg-icons';
 import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons';
-import MkModal from '@/components/ui/modal.vue';
 import Particle from '@/components/particle.vue';
 import * as os from '@/os';
 import { isDeviceTouch } from '@/scripts/is-device-touch';
@@ -104,14 +101,7 @@ import { isMobile } from '@/scripts/is-mobile';
 import { emojiCategories } from '@/instance';
 
 export default defineComponent({
-	components: {
-		MkModal,
-	},
-
 	props: {
-		src: {
-			required: false
-		},
 		showPinned: {
 			required: false,
 			default: true
@@ -121,7 +111,7 @@ export default defineComponent({
 		},
 	},
 
-	emits: ['done', 'closed'],
+	emits: ['chosen'],
 
 	data() {
 		return {
@@ -345,8 +335,7 @@ export default defineComponent({
 			}
 
 			const key = this.getKey(emoji);
-			this.$emit('done', key);
-			this.$refs.modal.close();
+			this.$emit('chosen', key);
 
 			// 最近使った絵文字更新
 			if (!this.pinned.includes(key)) {
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index 83157f7d59..e1927133ae 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -523,7 +523,7 @@ export default defineComponent({
 		react(viaKeyboard = false) {
 			pleaseLogin();
 			this.blur();
-			os.popup(import('@/components/emoji-picker.vue'), {
+			os.popup(import('@/components/emoji-picker-dialog.vue'), {
 				src: this.$refs.reactButton,
 				asReactionPicker: true
 			}, {
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 377e5991b2..6af0668e2e 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -498,7 +498,7 @@ export default defineComponent({
 		react(viaKeyboard = false) {
 			pleaseLogin();
 			this.blur();
-			os.popup(import('@/components/emoji-picker.vue'), {
+			os.popup(import('@/components/emoji-picker-dialog.vue'), {
 				src: this.$refs.reactButton,
 				asReactionPicker: true
 			}, {
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 7849095ba8..9b5adf9cc1 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -606,9 +606,7 @@ export default defineComponent({
 		},
 
 		async insertEmoji(ev) {
-			os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
-				insertTextAtCursor(this.$refs.text, emoji);
-			});
+			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
 		},
 
 		showActions(ev) {
diff --git a/src/client/components/ui/window.vue b/src/client/components/ui/window.vue
index d8852b9ffd..4d3b048c11 100644
--- a/src/client/components/ui/window.vue
+++ b/src/client/components/ui/window.vue
@@ -1,8 +1,8 @@
 <template>
 <transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
-	<div class="ebkgocck" v-if="showing">
+	<div class="ebkgocck" :class="{ front }" v-if="showing">
 		<div class="body _popup _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
-			<div class="header" @contextmenu.prevent.stop="onContextmenu">
+			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
 				<slot v-if="closeRight" name="buttons"><button class="_button" style="pointer-events: none;"></button></slot>
 				<button v-else class="_button" @click="close()"><Fa :icon="faTimes"/></button>
 
@@ -92,6 +92,16 @@ export default defineComponent({
 			required: false,
 			default: false,
 		},
+		mini: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		front: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
 		contextmenu: {
 			type: Array,
 			required: false,
@@ -387,6 +397,10 @@ export default defineComponent({
 	left: 0;
 	z-index: 5000;
 
+	&.front {
+		z-index: 11000; // front指定の時は、mk-modalのよりも大きくなければならない
+	}
+
 	> .body {
 		overflow: hidden; // overflow: clip; をSafariが対応したら消す
 		overflow: clip;
@@ -397,17 +411,22 @@ export default defineComponent({
     height: 100%;
 
 		> .header {
-			$height: 50px;
+			--height: 50px;
+
+			&.mini {
+				--height: 38px;
+			}
+
 			display: flex;
 			position: relative;
 			z-index: 1;
 			flex-shrink: 0;
 			user-select: none;
-			height: $height;
+			height: var(--height);
 
 			> ::v-deep(button) {
-				height: $height;
-				width: $height;
+				height: var(--height);
+				width: var(--height);
 
 				&:hover {
 					color: var(--fgHighlighted);
@@ -417,7 +436,7 @@ export default defineComponent({
 			> .title {
 				flex: 1;
 				position: relative;
-				line-height: $height;
+				line-height: var(--height);
 				white-space: nowrap;
 				overflow: hidden; // overflow: clip; をSafariが対応したら消す
 				overflow: clip;
diff --git a/src/client/os.ts b/src/client/os.ts
index 59ee3617e1..fe4054e272 100644
--- a/src/client/os.ts
+++ b/src/client/os.ts
@@ -1,5 +1,8 @@
+// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
+
 import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
 import { EventEmitter } from 'eventemitter3';
+import insertTextAtCursor from 'insert-text-at-cursor';
 import * as Sentry from '@sentry/browser';
 import Stream from '@/scripts/stream';
 import { apiUrl, debug } from '@/config';
@@ -289,7 +292,7 @@ export async function selectDriveFolder(multiple: boolean) {
 
 export async function pickEmoji(src?: HTMLElement, opts) {
 	return new Promise((resolve, reject) => {
-		popup(import('@/components/emoji-picker.vue'), {
+		popup(import('@/components/emoji-picker-dialog.vue'), {
 			src,
 			...opts
 		}, {
@@ -300,6 +303,63 @@ export async function pickEmoji(src?: HTMLElement, opts) {
 	});
 }
 
+type AwaitType<T> =
+	T extends Promise<infer U> ? U :
+	T extends (...args: Array<any>) => Promise<infer V> ? V :
+	T;
+let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
+let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
+export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) {
+	if (openingEmojiPicker) return;
+
+	activeTextarea = initialTextarea;
+
+	const textareas = document.querySelectorAll('textarea, input');
+	for (const textarea of Array.from(textareas)) {
+		textarea.addEventListener('focus', () => {
+			activeTextarea = textarea;
+		});
+	}
+
+	const observer = new MutationObserver(records => {
+		for (const record of records) {
+			for (const node of Array.from(record.addedNodes)) {
+				if (node instanceof HTMLElement) {
+					const textareas = node.querySelectorAll('textarea, input');
+					for (const textarea of Array.from(textareas)) {
+						if (textarea.dataset.preventEmojiInsert != null) return;
+						if (document.activeElement === textarea) activeTextarea = textarea;
+						textarea.addEventListener('focus', () => {
+							activeTextarea = textarea;
+						});
+					}
+				}
+			}
+		}
+	});
+
+	observer.observe(document.body, {
+		childList: true,
+		subtree: true,
+		attributes: false,
+		characterData: false,
+	});
+
+	openingEmojiPicker = await popup(import('@/components/emoji-picker-window.vue'), {
+		src,
+		...opts
+	}, {
+		chosen: emoji => {
+			insertTextAtCursor(activeTextarea, emoji);
+		},
+		closed: () => {
+			openingEmojiPicker!.dispose();
+			openingEmojiPicker = null;
+			observer.disconnect();
+		}
+	});
+}
+
 export function modalMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
 	return new Promise((resolve, reject) => {
 		let dispose;
diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue
index 258300dc52..5e675a3f12 100644
--- a/src/client/pages/messaging/messaging-room.form.vue
+++ b/src/client/pages/messaging/messaging-room.form.vue
@@ -223,9 +223,7 @@ export default defineComponent({
 		},
 
 		async insertEmoji(ev) {
-			os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
-				insertTextAtCursor(this.$refs.text, emoji);
-			});
+			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
 		}
 	}
 });
diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue
index 798ffb33d1..4488e50051 100644
--- a/src/client/pages/settings/reaction.vue
+++ b/src/client/pages/settings/reaction.vue
@@ -105,7 +105,7 @@ export default defineComponent({
 		},
 
 		preview(ev) {
-			os.popup(import('@/components/emoji-picker.vue'), {
+			os.popup(import('@/components/emoji-picker-dialog.vue'), {
 				asReactionPicker: true,
 				src: ev.currentTarget || ev.target,
 			}, {}, 'closed');
diff --git a/src/client/style.scss b/src/client/style.scss
index 3ebb5fabd1..aa2face04e 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -391,8 +391,8 @@ hr {
 
 ._acrylic {
 	background: var(--acrylicPanel);
-	-webkit-backdrop-filter: blur(10px);
-	backdrop-filter: blur(10px);
+	-webkit-backdrop-filter: blur(15px);
+	backdrop-filter: blur(15px);
 }
 
 ._vMargin {
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
index 3c5c1acfdc..d80978e182 100644
--- a/src/client/ui/chat/note.vue
+++ b/src/client/ui/chat/note.vue
@@ -504,7 +504,7 @@ export default defineComponent({
 			pleaseLogin();
 			this.operating = true;
 			this.blur();
-			const { dispose } = await os.popup(import('@/components/emoji-picker.vue'), {
+			const { dispose } = await os.popup(import('@/components/emoji-picker-dialog.vue'), {
 				src: this.$refs.reactButton,
 				asReactionPicker: true
 			}, {
diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue
index b0a31b097d..9388989d5d 100644
--- a/src/client/ui/chat/post-form.vue
+++ b/src/client/ui/chat/post-form.vue
@@ -593,9 +593,7 @@ export default defineComponent({
 		},
 
 		async insertEmoji(ev) {
-			os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
-				insertTextAtCursor(this.$refs.text, emoji);
-			});
+			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
 		},
 
 		showActions(ev) {
-- 
cgit v1.3.1-freya


From 764a158cd7e112186cbf54cb77599bcd22ea7d69 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 28 Feb 2021 01:09:59 +0900
Subject: Resolve #7270

---
 src/client/components/emoji-picker-dialog.vue |  9 +++++--
 src/client/components/emoji-picker.vue        | 35 +++++++++++--------------
 src/client/components/note-detailed.vue       | 22 ++++++----------
 src/client/components/note.vue                | 22 ++++++----------
 src/client/components/ui/modal.vue            | 26 +++++++++++++------
 src/client/os.ts                              | 37 +++++++++++++++++++++++++++
 src/client/ui/chat/note.vue                   | 25 ++++++------------
 7 files changed, 101 insertions(+), 75 deletions(-)

(limited to 'src/client/components/note.vue')

diff --git a/src/client/components/emoji-picker-dialog.vue b/src/client/components/emoji-picker-dialog.vue
index 177b5db44d..3450d219c3 100644
--- a/src/client/components/emoji-picker-dialog.vue
+++ b/src/client/components/emoji-picker-dialog.vue
@@ -1,6 +1,6 @@
 <template>
-<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
-	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
+<MkModal ref="modal" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="$refs.picker.focus()" @close="$emit('close')" @closed="$emit('closed')">
+	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen" ref="picker"/>
 </MkModal>
 </template>
 
@@ -16,6 +16,11 @@ export default defineComponent({
 	},
 
 	props: {
+		manualShowing: {
+			type: Boolean,
+			required: false,
+			default: null,
+		},
 		src: {
 			required: false
 		},
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
index b11f0a62fa..41e667dd98 100644
--- a/src/client/components/emoji-picker.vue
+++ b/src/client/components/emoji-picker.vue
@@ -54,23 +54,17 @@
 				</div>
 			</section>
 		</div>
-		<div v-appear="() => showingCustomEmojis = true">
+		<div>
 			<header class="_acrylic">{{ $ts.customEmojis }}</header>
-			<template v-if="showingCustomEmojis">
-				<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
-			</template>
+			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
 		</div>
-		<div v-appear="() => showingEmojis = true">
+		<div>
 			<header class="_acrylic">{{ $ts.emoji }}</header>
-			<template v-if="showingEmojis">
-				<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
-			</template>
+			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
 		</div>
-		<div v-appear="() => showingTags = true">
+		<div>
 			<header class="_acrylic">{{ $ts.tags }}</header>
-			<template v-if="showingTags">
-				<XSection v-for="tag in emojiTags" :emojis="customEmojis.filter(e => e.aliases.includes(tag)).map(e => ':' + e.name + ':')">{{ tag }}</XSection>
-			</template>
+			<XSection v-for="tag in emojiTags" :emojis="customEmojis.filter(e => e.aliases.includes(tag)).map(e => ':' + e.name + ':')">{{ tag }}</XSection>
 		</div>
 	</div>
 	<div class="tabs">
@@ -127,9 +121,6 @@ export default defineComponent({
 			searchResultCustom: [],
 			searchResultUnicode: [],
 			tab: 'index',
-			showingCustomEmojis: false,
-			showingEmojis: false,
-			showingTags: false,
 			categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
 			faGlobe, faClock, faChevronDown, faAsterisk, faLaugh, faUtensils, faLeaf, faShapes, faBicycle, faHashtag,
 		};
@@ -279,14 +270,18 @@ export default defineComponent({
 	},
 
 	mounted() {
-		if (!isMobile && !isDeviceTouch) {
-			this.$refs.search.focus({
-				preventScroll: true
-			});
-		}
+		this.focus();
 	},
 
 	methods: {
+		focus() {
+			if (!isMobile && !isDeviceTouch) {
+				this.$refs.search.focus({
+					preventScroll: true
+				});
+			}
+		},
+
 		getKey(emoji: any) {
 			return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
 		},
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index e1927133ae..7df87c6b01 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -523,20 +523,14 @@ export default defineComponent({
 		react(viaKeyboard = false) {
 			pleaseLogin();
 			this.blur();
-			os.popup(import('@/components/emoji-picker-dialog.vue'), {
-				src: this.$refs.reactButton,
-				asReactionPicker: true
-			}, {
-				done: reaction => {
-					if (reaction) {
-						os.api('notes/reactions/create', {
-							noteId: this.appearNote.id,
-							reaction: reaction
-						});
-					}
-					this.focus();
-				},
-			}, 'closed');
+			os.pickReaction(this.$refs.reactButton, reaction => {
+				os.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				});
+			}, () => {
+				this.focus();
+			});
 		},
 
 		reactDirectly(reaction) {
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 6af0668e2e..dab7643762 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -498,20 +498,14 @@ export default defineComponent({
 		react(viaKeyboard = false) {
 			pleaseLogin();
 			this.blur();
-			os.popup(import('@/components/emoji-picker-dialog.vue'), {
-				src: this.$refs.reactButton,
-				asReactionPicker: true
-			}, {
-				done: reaction => {
-					if (reaction) {
-						os.api('notes/reactions/create', {
-							noteId: this.appearNote.id,
-							reaction: reaction
-						});
-					}
-					this.focus();
-				},
-			}, 'closed');
+			os.pickReaction(this.$refs.reactButton, reaction => {
+				os.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				});
+			}, () => {
+				this.focus();
+			});
 		},
 
 		reactDirectly(reaction) {
diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue
index 405fa4aaa5..1c8ae63907 100644
--- a/src/client/components/ui/modal.vue
+++ b/src/client/components/ui/modal.vue
@@ -1,11 +1,13 @@
 <template>
-<div class="mk-modal" v-hotkey.global="keymap" :style="{ pointerEvents: showing ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+<div class="mk-modal" v-hotkey.global="keymap" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 	<transition :name="$store.state.animation ? 'modal-bg' : ''" appear>
-		<div class="bg _modalBg" v-if="showing" @click="onBgClick"></div>
+		<div class="bg _modalBg" v-if="manualShowing != null ? manualShowing : showing" @click="onBgClick"></div>
 	</transition>
 	<div class="content" :class="{ popup, fixed, top: position === 'top' }" @click.self="onBgClick" ref="content">
-		<transition :name="$store.state.animation ? popup ? 'modal-popup-content' : 'modal-content' : ''" appear @after-leave="$emit('closed')" @after-enter="childRendered">
-			<slot v-if="showing"></slot>
+		<transition :name="$store.state.animation ? popup ? 'modal-popup-content' : 'modal-content' : ''" appear @after-leave="$emit('closed')" @enter="$emit('opening')" @after-enter="childRendered">
+			<div v-show="manualShowing != null ? manualShowing : showing">
+				<slot></slot>
+			</div>
 		</transition>
 	</div>
 </div>
@@ -29,6 +31,11 @@ export default defineComponent({
 		modal: true
 	},
 	props: {
+		manualShowing: {
+			type: Boolean,
+			required: false,
+			default: null,
+		},
 		srcCenter: {
 			type: Boolean,
 			required: false
@@ -40,7 +47,7 @@ export default defineComponent({
 			required: false
 		}
 	},
-	emits: ['click', 'esc', 'closed'],
+	emits: ['opening', 'click', 'esc', 'close', 'closed'],
 	data() {
 		return {
 			showing: true,
@@ -60,15 +67,17 @@ export default defineComponent({
 		}
 	},
 	mounted() {
-		this.fixed = getFixedContainer(this.src) != null;
+		this.$watch('src', () => {
+			this.fixed = getFixedContainer(this.src) != null;
+		}, { immediate: true });
 
 		this.$nextTick(() => {
-			if (!this.popup) return;
-
 			const popover = this.$refs.content as any;
 
 			// TODO: ResizeObserver無くしたい
 			new ResizeObserver((entries, observer) => {
+				if (!this.popup) return;
+
 				const rect = this.src.getBoundingClientRect();
 				
 				const width = popover.offsetWidth;
@@ -141,6 +150,7 @@ export default defineComponent({
 
 		close() {
 			this.showing = false;
+			this.$emit('close');
 		},
 
 		onBgClick() {
diff --git a/src/client/os.ts b/src/client/os.ts
index a971eebd45..9fafb6db43 100644
--- a/src/client/os.ts
+++ b/src/client/os.ts
@@ -357,6 +357,43 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
 	});
 }
 
+let reactionPicker = null;
+export async function pickReaction(src: HTMLElement, chosen, closed) {
+	if (reactionPicker) {
+		if (reactionPicker.opening) return;
+
+		reactionPicker.opening = true;
+		reactionPicker.src.value = src;
+		reactionPicker.manualShowing.value = true;
+		reactionPicker.chosen = chosen;
+		reactionPicker.closed = closed;
+	} else {
+		reactionPicker = {
+			opening: true,
+			src: ref(src),
+			manualShowing: ref(true),
+			chosen, closed
+		};
+		popup(import('@/components/emoji-picker-dialog.vue'), {
+			src: reactionPicker.src,
+			asReactionPicker: true,
+			manualShowing: reactionPicker.manualShowing
+		}, {
+			done: reaction => {
+				reactionPicker.chosen(reaction);
+			},
+			close: () => {
+				reactionPicker.manualShowing.value = false;
+			},
+			closed: () => {
+				reactionPicker.src.value = null;
+				reactionPicker.closed();
+				reactionPicker.opening = false;
+			}
+		});
+	}
+}
+
 export function modalMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
 	return new Promise((resolve, reject) => {
 		let dispose;
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
index d80978e182..75b92a32f2 100644
--- a/src/client/ui/chat/note.vue
+++ b/src/client/ui/chat/note.vue
@@ -504,23 +504,14 @@ export default defineComponent({
 			pleaseLogin();
 			this.operating = true;
 			this.blur();
-			const { dispose } = await os.popup(import('@/components/emoji-picker-dialog.vue'), {
-				src: this.$refs.reactButton,
-				asReactionPicker: true
-			}, {
-				done: reaction => {
-					if (reaction) {
-						os.api('notes/reactions/create', {
-							noteId: this.appearNote.id,
-							reaction: reaction
-						});
-					}
-				},
-				closed: () => {
-					this.operating = false;
-					this.focus();
-					dispose();
-				}
+			os.pickReaction(this.$refs.reactButton, reaction => {
+				os.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				});
+			}, () => {
+				this.operating = false;
+				this.focus();
 			});
 		},
 
-- 
cgit v1.3.1-freya

