Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #6925 - Emoji selection when typing : (colon) symbol in the chat box #6991

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
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
75 changes: 75 additions & 0 deletions plugins/text-editor-resources/src/Completion.ts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Non-blocking comment]: In this file I have seen 3 eslint-disable-next-line. I do not wanna say do not use it, but rather I prefer to stay safe as much as possible. So for example if the type is an issue I try first to fix that and not use eslint-disable-next-line.

Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,78 @@ export const Completion = Node.create<CompletionOptions>({
]
}
})
export const CompletionEmoji = Node.create<CompletionOptions>({
name: 'completion-emoji',

addOptions () {
return {
HTMLAttributes: {},
renderLabel ({ options, node }) {
// eslint-disable-next-line
return `${node.attrs.objectclass}`
},
suggestion: {
char: ':',
allowSpaces: false,
command: ({ editor, range, props }) => {
// increase range.to by one when the next node is of type "text"
// and starts with a space character
const nodeAfter = editor.view.state.selection.$to.nodeAfter
const overrideSpace = nodeAfter?.text?.startsWith(' ')

if (overrideSpace !== undefined && overrideSpace) {
// eslint-disable-next-line
range.to += 1
}

if (props !== null) {
editor
.chain()
.focus()
.insertContentAt(range, [
{
type: 'text',
text: props.objectclass
},
{
type: 'text',
text: ' '
}
])
.run()
}
},
allow: ({ editor, range }) => {
if (range.from > editor.state.doc.content.size) {
return false
}
const $from = editor.state.doc.resolve(range.from)
const type = editor.schema.nodes[this.name]
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
return !!$from.parent.type.contentMatch.matchType(type)
}
}
}
},

group: 'inline',

inline: true,

selectable: true,

atom: true,

draggable: true,

addProseMirrorPlugins () {
return [
Suggestion({
editor: this.editor,
pluginKey: new PluginKey('completion-suggestion-emoji'),
...this.options.suggestion
})

]
}
})
48 changes: 48 additions & 0 deletions plugins/text-editor-resources/src/components/EmojiList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2022, 2023, 2024 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 EmojiPopup from './EmojiPopup.svelte'
import Popup from './Popup.svelte'

export let query = ''
export let clientRect: () => ClientRect
export let command: (props: any) => void
export let close: () => void

let searchPopup: EmojiPopup

function dispatchItem(item?: { id: string; objectclass: string }): void {
if (item == null) {
close()
} else {
command(item)
}
}

export function onKeyDown(ev: KeyboardEvent): boolean {
return searchPopup.onKeyDown(ev)
}
</script>

<Popup {query} {clientRect} {command} {close}>
<EmojiPopup
bind:this={searchPopup}
{query}
on:close={(evt) => {
dispatchItem(evt.detail)
}}
/>
</Popup>
134 changes: 134 additions & 0 deletions plugins/text-editor-resources/src/components/EmojiPopup.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2023, 2024 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 presentation, { reduceCalls } from '@hcengineering/presentation'
import { Label, ListView, resizeObserver } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import emojiReplaceDict from './extension/emojiIdMap.json'

export let query = ''

let items = Object.entries(emojiReplaceDict)

const dispatch = createEventDispatcher()

let list: ListView
let scrollContainer: HTMLElement
let selection = 0

function dispatchItem(item: [string, string]): void {
dispatch('close', {
id: item[0],
objectclass: item[1]
})
}

export function onKeyDown(key: KeyboardEvent): boolean {
if (key.key === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list?.select(selection + 1)
return true
}
if (key.key === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
if (selection === 0 && scrollContainer !== undefined) {
scrollContainer.scrollTop = 0
}
list?.select(selection - 1)
return true
}
if (key.key === 'Enter' || key.key === 'Tab') {
key.preventDefault()
key.stopPropagation()
if (selection < items.length) {
const searchItem = items[selection]
dispatchItem(searchItem)
return true
} else {
return false
}
}
return false
}

const updateItems = reduceCalls(async function (localQuery: string): Promise<void> {
items = Object.entries(emojiReplaceDict).filter(([k, v]) => k.includes(localQuery))
})
$: void updateItems(query)
</script>

{#if (items.length === 0 && query !== '') || items.length > 0}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<form class="antiPopup mentionPoup" on:keydown={onKeyDown} use:resizeObserver={() => dispatch('changeSize')}>
<div class="ap-scroll" bind:this={scrollContainer}>
<div class="ap-box">
{#if items.length === 0 && query !== ''}
<div class="noResults"><Label label={presentation.string.NoResults} /></div>
{/if}
{#if items.length > 0}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<ListView bind:this={list} bind:selection count={items.length}>
<svelte:fragment slot="item" let:item={num}>
{@const item = items[num]}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="ap-menuItem withComp h-8"
on:click={() => {
dispatchItem(item)
}}
>
<div class="flex-row-center">
<div class="flex-center p-1 content-dark-color flex-no-shrink">
{item[1]}
</div>
<span class="ml-1 max-w-120 overflow-label searchResult">
<span class="name">{item[0]}</span>
</span>
</div>
</div>
Comment on lines +89 to +103

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semantic html please!

I cannot help you with that here since I am not fully aware of the context. But the least we could do for accessibility is to add aria-* or other html attributes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did how they did.
https://github.com/ElSalvo96/platform/blob/4e7673cb5b1af10e66cb84f05fba3790494b05d4/packages/presentation/src/components/SearchResult.svelte#L27-L46

What kind of improvement do you expect with the aria-* tag? I think there you don't need any of these

</svelte:fragment>
</ListView>
{/if}
</div>
</div>
<div class="ap-space x2" />
</form>
{/if}

<style lang="scss">
.noResults {
display: flex;
padding: 0.25rem 1rem;
align-items: center;
align-self: stretch;
}

.mentionPoup {
padding-top: 0.5rem;
}

.searchResult {
display: flex;
flex-direction: row;

.name {
display: flex;
flex: 1;
}
ElSalvo96 marked this conversation as resolved.
Show resolved Hide resolved
}
</style>
Loading