parent
							
								
									f2e071baaa
								
							
						
					
					
						commit
						f29d417b30
					
				
					 13 changed files with 563 additions and 113 deletions
				
			
		
							
								
								
									
										191
									
								
								src/client/components/emoji-picker-dialog.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/client/components/emoji-picker-dialog.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||||
							
								
								
									
										197
									
								
								src/client/components/emoji-picker-window.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								src/client/components/emoji-picker-window.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||||
|  | @ -1,93 +1,91 @@ | ||||||
| <template> | <template> | ||||||
| <MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')"> | <div class="omfetrab _popup" :class="['w' + width, 'h' + height, { big }]"> | ||||||
| 	<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()"> | ||||||
| 		<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"> | ||||||
| 		<div class="emojis" ref="emojis"> | 		<section class="result"> | ||||||
| 			<section class="result"> | 			<div v-if="searchResultCustom.length > 0"> | ||||||
| 				<div v-if="searchResultCustom.length > 0"> | 				<button v-for="emoji in searchResultCustom" | ||||||
| 					<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" | 						class="_button" | ||||||
| 						:title="emoji.name" |  | ||||||
| 						@click="chosen(emoji, $event)" | 						@click="chosen(emoji, $event)" | ||||||
| 						:key="emoji" | 						:key="emoji" | ||||||
| 						tabindex="0" |  | ||||||
| 					> | 					> | ||||||
| 						<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/> | 						<MkEmoji :emoji="emoji" :normal="true"/> | ||||||
| 						<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> | 					</button> | ||||||
| 				</div> | 				</div> | ||||||
| 			</section> | 			</section> | ||||||
| 
 | 
 | ||||||
| 			<div class="index"> | 			<div class="arrow"><Fa :icon="faChevronDown"/></div> | ||||||
| 				<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> |  | ||||||
| 
 |  | ||||||
| 			<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> | 		</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> | 	</div> | ||||||
| </MkModal> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -96,7 +94,6 @@ import { emojilist } from '../../misc/emojilist'; | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | 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 { 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 { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import MkModal from '@/components/ui/modal.vue'; |  | ||||||
| import Particle from '@/components/particle.vue'; | import Particle from '@/components/particle.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { isDeviceTouch } from '@/scripts/is-device-touch'; | import { isDeviceTouch } from '@/scripts/is-device-touch'; | ||||||
|  | @ -104,14 +101,7 @@ import { isMobile } from '@/scripts/is-mobile'; | ||||||
| import { emojiCategories } from '@/instance'; | import { emojiCategories } from '@/instance'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { |  | ||||||
| 		MkModal, |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	props: { | 	props: { | ||||||
| 		src: { |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		showPinned: { | 		showPinned: { | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: true | 			default: true | ||||||
|  | @ -121,7 +111,7 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['done', 'closed'], | 	emits: ['chosen'], | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  | @ -345,8 +335,7 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			const key = this.getKey(emoji); | 			const key = this.getKey(emoji); | ||||||
| 			this.$emit('done', key); | 			this.$emit('chosen', key); | ||||||
| 			this.$refs.modal.close(); |  | ||||||
| 
 | 
 | ||||||
| 			// 最近使った絵文字更新 | 			// 最近使った絵文字更新 | ||||||
| 			if (!this.pinned.includes(key)) { | 			if (!this.pinned.includes(key)) { | ||||||
|  |  | ||||||
|  | @ -523,7 +523,7 @@ export default defineComponent({ | ||||||
| 		react(viaKeyboard = false) { | 		react(viaKeyboard = false) { | ||||||
| 			pleaseLogin(); | 			pleaseLogin(); | ||||||
| 			this.blur(); | 			this.blur(); | ||||||
| 			os.popup(import('@/components/emoji-picker.vue'), { | 			os.popup(import('@/components/emoji-picker-dialog.vue'), { | ||||||
| 				src: this.$refs.reactButton, | 				src: this.$refs.reactButton, | ||||||
| 				asReactionPicker: true | 				asReactionPicker: true | ||||||
| 			}, { | 			}, { | ||||||
|  |  | ||||||
|  | @ -498,7 +498,7 @@ export default defineComponent({ | ||||||
| 		react(viaKeyboard = false) { | 		react(viaKeyboard = false) { | ||||||
| 			pleaseLogin(); | 			pleaseLogin(); | ||||||
| 			this.blur(); | 			this.blur(); | ||||||
| 			os.popup(import('@/components/emoji-picker.vue'), { | 			os.popup(import('@/components/emoji-picker-dialog.vue'), { | ||||||
| 				src: this.$refs.reactButton, | 				src: this.$refs.reactButton, | ||||||
| 				asReactionPicker: true | 				asReactionPicker: true | ||||||
| 			}, { | 			}, { | ||||||
|  |  | ||||||
|  | @ -606,9 +606,7 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		async insertEmoji(ev) { | 		async insertEmoji(ev) { | ||||||
| 			os.pickEmoji(ev.currentTarget || ev.target).then(emoji => { | 			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); | ||||||
| 				insertTextAtCursor(this.$refs.text, emoji); |  | ||||||
| 			}); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		showActions(ev) { | 		showActions(ev) { | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')"> | <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="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> | 				<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> | 				<button v-else class="_button" @click="close()"><Fa :icon="faTimes"/></button> | ||||||
| 
 | 
 | ||||||
|  | @ -92,6 +92,16 @@ export default defineComponent({ | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false, | 			default: false, | ||||||
| 		}, | 		}, | ||||||
|  | 		mini: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
|  | 		front: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
| 		contextmenu: { | 		contextmenu: { | ||||||
| 			type: Array, | 			type: Array, | ||||||
| 			required: false, | 			required: false, | ||||||
|  | @ -387,6 +397,10 @@ export default defineComponent({ | ||||||
| 	left: 0; | 	left: 0; | ||||||
| 	z-index: 5000; | 	z-index: 5000; | ||||||
| 
 | 
 | ||||||
|  | 	&.front { | ||||||
|  | 		z-index: 11000; // front指定の時は、mk-modalのよりも大きくなければならない | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	> .body { | 	> .body { | ||||||
| 		overflow: hidden; // overflow: clip; をSafariが対応したら消す | 		overflow: hidden; // overflow: clip; をSafariが対応したら消す | ||||||
| 		overflow: clip; | 		overflow: clip; | ||||||
|  | @ -397,17 +411,22 @@ export default defineComponent({ | ||||||
|     height: 100%; |     height: 100%; | ||||||
| 
 | 
 | ||||||
| 		> .header { | 		> .header { | ||||||
| 			$height: 50px; | 			--height: 50px; | ||||||
|  | 
 | ||||||
|  | 			&.mini { | ||||||
|  | 				--height: 38px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
| 			display: flex; | 			display: flex; | ||||||
| 			position: relative; | 			position: relative; | ||||||
| 			z-index: 1; | 			z-index: 1; | ||||||
| 			flex-shrink: 0; | 			flex-shrink: 0; | ||||||
| 			user-select: none; | 			user-select: none; | ||||||
| 			height: $height; | 			height: var(--height); | ||||||
| 
 | 
 | ||||||
| 			> ::v-deep(button) { | 			> ::v-deep(button) { | ||||||
| 				height: $height; | 				height: var(--height); | ||||||
| 				width: $height; | 				width: var(--height); | ||||||
| 
 | 
 | ||||||
| 				&:hover { | 				&:hover { | ||||||
| 					color: var(--fgHighlighted); | 					color: var(--fgHighlighted); | ||||||
|  | @ -417,7 +436,7 @@ export default defineComponent({ | ||||||
| 			> .title { | 			> .title { | ||||||
| 				flex: 1; | 				flex: 1; | ||||||
| 				position: relative; | 				position: relative; | ||||||
| 				line-height: $height; | 				line-height: var(--height); | ||||||
| 				white-space: nowrap; | 				white-space: nowrap; | ||||||
| 				overflow: hidden; // overflow: clip; をSafariが対応したら消す | 				overflow: hidden; // overflow: clip; をSafariが対応したら消す | ||||||
| 				overflow: clip; | 				overflow: clip; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
|  | // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
 | ||||||
|  | 
 | ||||||
| import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue'; | import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue'; | ||||||
| import { EventEmitter } from 'eventemitter3'; | import { EventEmitter } from 'eventemitter3'; | ||||||
|  | import insertTextAtCursor from 'insert-text-at-cursor'; | ||||||
| import * as Sentry from '@sentry/browser'; | import * as Sentry from '@sentry/browser'; | ||||||
| import Stream from '@/scripts/stream'; | import Stream from '@/scripts/stream'; | ||||||
| import { apiUrl, debug } from '@/config'; | import { apiUrl, debug } from '@/config'; | ||||||
|  | @ -289,7 +292,7 @@ export async function selectDriveFolder(multiple: boolean) { | ||||||
| 
 | 
 | ||||||
| export async function pickEmoji(src?: HTMLElement, opts) { | export async function pickEmoji(src?: HTMLElement, opts) { | ||||||
| 	return new Promise((resolve, reject) => { | 	return new Promise((resolve, reject) => { | ||||||
| 		popup(import('@/components/emoji-picker.vue'), { | 		popup(import('@/components/emoji-picker-dialog.vue'), { | ||||||
| 			src, | 			src, | ||||||
| 			...opts | 			...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 }) { | export function modalMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) { | ||||||
| 	return new Promise((resolve, reject) => { | 	return new Promise((resolve, reject) => { | ||||||
| 		let dispose; | 		let dispose; | ||||||
|  |  | ||||||
|  | @ -223,9 +223,7 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		async insertEmoji(ev) { | 		async insertEmoji(ev) { | ||||||
| 			os.pickEmoji(ev.currentTarget || ev.target).then(emoji => { | 			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); | ||||||
| 				insertTextAtCursor(this.$refs.text, emoji); |  | ||||||
| 			}); |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -105,7 +105,7 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		preview(ev) { | 		preview(ev) { | ||||||
| 			os.popup(import('@/components/emoji-picker.vue'), { | 			os.popup(import('@/components/emoji-picker-dialog.vue'), { | ||||||
| 				asReactionPicker: true, | 				asReactionPicker: true, | ||||||
| 				src: ev.currentTarget || ev.target, | 				src: ev.currentTarget || ev.target, | ||||||
| 			}, {}, 'closed'); | 			}, {}, 'closed'); | ||||||
|  |  | ||||||
|  | @ -391,8 +391,8 @@ hr { | ||||||
| 
 | 
 | ||||||
| ._acrylic { | ._acrylic { | ||||||
| 	background: var(--acrylicPanel); | 	background: var(--acrylicPanel); | ||||||
| 	-webkit-backdrop-filter: blur(10px); | 	-webkit-backdrop-filter: blur(15px); | ||||||
| 	backdrop-filter: blur(10px); | 	backdrop-filter: blur(15px); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ._vMargin { | ._vMargin { | ||||||
|  |  | ||||||
|  | @ -504,7 +504,7 @@ export default defineComponent({ | ||||||
| 			pleaseLogin(); | 			pleaseLogin(); | ||||||
| 			this.operating = true; | 			this.operating = true; | ||||||
| 			this.blur(); | 			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, | 				src: this.$refs.reactButton, | ||||||
| 				asReactionPicker: true | 				asReactionPicker: true | ||||||
| 			}, { | 			}, { | ||||||
|  |  | ||||||
|  | @ -593,9 +593,7 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		async insertEmoji(ev) { | 		async insertEmoji(ev) { | ||||||
| 			os.pickEmoji(ev.currentTarget || ev.target).then(emoji => { | 			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); | ||||||
| 				insertTextAtCursor(this.$refs.text, emoji); |  | ||||||
| 			}); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		showActions(ev) { | 		showActions(ev) { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue