Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions packages/ui/src/components/emoji/EmojiGroup.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->

<script lang="ts">
import { Label, Lazy } from '../../index'
import EmojiGroupPalette from './EmojiGroupPalette.svelte'
import { resultEmojis, EmojiWithGroup, EmojiCategory } from '.'

import plugin from '../../plugin'

export let group: EmojiCategory
export let lazy: boolean = true
export let searching: boolean = false
export let disabled: boolean = false
export let selected: string | undefined = undefined
export let skinTone: number = 0
export let kind: 'default' | 'fade' = 'fade'

let emojis: EmojiWithGroup[] = []
$: emojis = searching
? $resultEmojis
: Array.isArray(group.emojis)
? group.emojis
: $resultEmojis.filter((re) => re.key === group.id)
</script>

<div class="hulyPopupEmoji-group kind-{kind}" class:lazy>
<div id={group.id} class="hulyPopupEmoji-group__header categoryHeader">
<Label label={searching && $resultEmojis.length === 0 ? plugin.string.NoResults : group.label} />
</div>
{#if lazy}
<Lazy>
<EmojiGroupPalette {emojis} {disabled} {selected} {skinTone} on:select on:touchstart on:contextmenu />
</Lazy>
{:else}
<EmojiGroupPalette {emojis} {disabled} {selected} {skinTone} on:select on:touchstart on:contextmenu />
{/if}
</div>

<style lang="scss">
.hulyPopupEmoji-group {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-width: 0;
min-height: 0;

&.lazy {
min-height: 10rem;
}

&__header {
position: sticky;
flex-shrink: 0;
margin: 0.75rem 0.75rem 0.25rem;
padding: 0.25rem 0.375rem;
top: 0;
height: 1.5rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
text-shadow: 0 0 0.25rem var(--theme-popup-color);
color: var(--theme-caption-color);
border-radius: 0.25rem;
z-index: 1;
pointer-events: none;

&:first-child {
margin-top: 0;
}
&::before {
content: '';
position: absolute;
top: -1px;
left: 0;
width: 100%;
height: 150%;
background: var(--theme-popup-trans-gradient);
z-index: -1;
}
}

&.kind-default {
.hulyPopupEmoji-group__header {
background: var(--theme-popup-header);

&::before {
content: none;
}
}
}
}
</style>
58 changes: 58 additions & 0 deletions packages/ui/src/components/emoji/EmojiGroupPalette.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->

<script lang="ts">
import { createEventDispatcher } from 'svelte'
import type { EmojiWithGroup } from '.'

import EmojiButton from './EmojiButton.svelte'

export let emojis: EmojiWithGroup[]
export let selected: string | undefined
export let disabled: boolean = false
export let skinTone: number = 0

const dispatch = createEventDispatcher()
</script>

<div class="hulyPopupEmoji-group__palette">
{#each emojis as emoji}
<EmojiButton
{emoji}
selected={emoji.emoji === selected}
{disabled}
{skinTone}
on:select
on:touchstart={(event) => {
dispatch('touchstart', { event, emoji })
}}
on:contextmenu={(event) => {
dispatch('contextmenu', { event, emoji })
}}
/>
{/each}
<div class="clear-mins flex-grow" />
</div>

<style lang="scss">
.hulyPopupEmoji-group__palette {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
flex-shrink: 0;
margin-inline: 0.75rem;
font-size: 1.25rem;
}
</style>
146 changes: 32 additions & 114 deletions packages/ui/src/components/emoji/EmojiPopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,18 @@
import { createEventDispatcher, onMount, onDestroy } from 'svelte'
import type { Emoji } from 'emojibase'
import {
Label,
Scroller,
SearchInput,
tooltip,
deviceOptionsStore as deviceInfo,
showPopup,
eventToHTMLElement,
ButtonBase,
closeTooltip,
Spinner
closeTooltip
} from '../../'
import plugin from '../../plugin'
import {
searchEmoji,
resultEmojis,
emojiStore,
generateSkinToneEmojis,
emojiCategories,
Expand All @@ -32,18 +29,17 @@
setSkinTone
} from '.'
import type { EmojiWithGroup, EmojiCategory } from '.'
import EmojiButton from './EmojiButton.svelte'
import ActionsPopup from './ActionsPopup.svelte'
import SkinTonePopup from './SkinTonePopup.svelte'
import IconSearch from './icons/Search.svelte'
import EmojiGroup from './EmojiGroup.svelte'

export let embedded = false
export let selected: string | undefined
export let disabled: boolean = false
export let kind: 'default' | 'fade' = 'fade'

const dispatch = createEventDispatcher()
closeTooltip()

let scrollElement: HTMLDivElement
const touchEvents = ['touchend', 'touchcancel', 'touchmove']
Expand All @@ -63,7 +59,6 @@
let timer: any = null
const isMobile = $deviceInfo.isMobile

let loading: boolean = true
let emojisCat = emojiCategories
let currentCategory = emojisCat[0]
$: emojiTabs = emojisCat
Expand Down Expand Up @@ -212,11 +207,11 @@
(em) => em.categories !== undefined || (Array.isArray(em.emojis) && em.emojis.length > 0)
)
currentCategory = emojisCat.find((ec) => ec.emojis !== undefined) ?? emojisCat[0]
loading = false
}

onMount(() => {
if (scrollElement !== undefined) scrollElement.addEventListener('scroll', checkScroll)
closeTooltip()
setTimeout(initEmoji)
})
onDestroy(() => {
Expand All @@ -229,15 +224,14 @@
$: outputGroups = updateGroups(searching, emojisCat)
</script>

<div class="hulyPopupEmoji-container kind-{kind}" class:embedded class:loading>
<div class="hulyPopupEmoji-container kind-{kind}" class:embedded>
<div class="hulyPopupEmoji-header__tabs-wrapper">
<div class="hulyPopupEmoji-header__tabs">
{#each categoryTabs as category (category.id)}
<button
class="hulyPopupEmoji-header__tab"
class:selected={(searching && searchCategory[0].id === category.id) ||
(!searching && currentCategory.id === category.id)}
disabled={loading}
data-id={category.id}
use:tooltip={{ label: category.label }}
on:click={() => {
Expand Down Expand Up @@ -279,42 +273,34 @@
<span style:font-size={'1.5rem'}>{generateSkinToneEmojis(0x1f590)[skinTone]}</span>
</ButtonBase>
</div>
<Scroller bind:divScroll={scrollElement} checkForHeaders noStretch on:scrolledCategories={handleCategoryScrolled}>
{#if loading}
<div class="flex-center w-full h-full min-h-full clear-mins flex-grow"><Spinner /></div>
{:else}
{#each outputGroups as group (group.id)}
{@const canRemove = group.id === 'frequently-used'}
{@const emojisGroup = searching
? $resultEmojis
: Array.isArray(group.emojis)
? group.emojis
: $resultEmojis.filter((re) => re.key === group.id)}
<div class="hulyPopupEmoji-group">
<div id={group.id} class="hulyPopupEmoji-group__header categoryHeader">
<Label label={searching && $resultEmojis.length === 0 ? plugin.string.NoResults : group.label} />
</div>
<div class="hulyPopupEmoji-group__palette">
{#each emojisGroup as emoji}
<EmojiButton
{emoji}
selected={emoji.emoji === selected}
{disabled}
{skinTone}
on:select={selectedEmoji}
on:touchstart={(event) => {
clampedContextMenu(event, emoji, canRemove)
}}
on:contextmenu={(event) => {
handleContextMenu(event, emoji, canRemove)
}}
/>
{/each}
<div class="clear-mins flex-grow" />
</div>
</div>
{/each}
{/if}
<Scroller
bind:divScroll={scrollElement}
gap="0.5rem"
checkForHeaders
noStretch
on:scrolledCategories={handleCategoryScrolled}
>
{#each outputGroups as group (group.id)}
{@const canRemove = group.id === 'frequently-used'}
<EmojiGroup
{group}
{searching}
{disabled}
{selected}
{skinTone}
{kind}
lazy={group.id !== 'frequently-used'}
on:select={selectedEmoji}
on:touchstart={(ev) => {
const { event, emoji } = ev.detail
clampedContextMenu(event, emoji, canRemove)
}}
on:contextmenu={(ev) => {
const { event, emoji } = ev.detail
handleContextMenu(event, emoji, canRemove)
}}
/>
{/each}
</Scroller>
{#if !hidden && kind === 'fade'}<div class="hulyPopupEmoji-footer" />{/if}
</div>
Expand Down Expand Up @@ -428,55 +414,6 @@
padding: 0 0.75rem;
}

.hulyPopupEmoji-group {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-width: 0;
min-height: 0;

&__header {
position: sticky;
flex-shrink: 0;
margin: 0.75rem 0.75rem 0.25rem;
padding: 0.25rem 0.375rem;
top: 0;
height: 1.5rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
text-shadow: 0 0 0.25rem var(--theme-popup-color);
color: var(--theme-caption-color);
border-radius: 0.25rem;
z-index: 1;
pointer-events: none;

&:first-child {
margin-top: 0;
}
&::before {
content: '';
position: absolute;
top: -1px;
left: 0;
width: 100%;
height: 150%;
background: var(--theme-popup-trans-gradient);
z-index: -1;
}
}
&__palette {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
flex-shrink: 0;
margin-inline: 0.75rem;
font-size: 1.25rem;
}
}
.hulyPopupEmoji-group + .hulyPopupEmoji-group {
margin-top: 0.5rem;
}
.hulyPopupEmoji-footer {
position: absolute;
left: 50%;
Expand All @@ -489,24 +426,5 @@
z-index: 1;
pointer-events: none;
}

&.kind-default {
.hulyPopupEmoji-group__header {
background: var(--theme-popup-header);

&::before {
content: none;
}
}
}

&.loading {
.hulyPopupEmoji-header__tab {
color: var(--theme-trans-color);
}
:global(.scroll) {
justify-content: center;
}
}
}
</style>