Add emoji text node

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
Anton Alexeyev 2025-04-29 15:29:03 +07:00
parent 76a425ba98
commit 4d1b2a436d
15 changed files with 111 additions and 7 deletions

View File

@ -19,6 +19,7 @@
import NodeContent from './NodeContent.svelte' import NodeContent from './NodeContent.svelte'
export let node: MarkupNode export let node: MarkupNode
export let single = true
export let preview = false export let preview = false
</script> </script>
@ -27,9 +28,9 @@
{#if marks.length > 0} {#if marks.length > 0}
<NodeMarks {marks}> <NodeMarks {marks}>
<NodeContent {node} {preview} /> <NodeContent {node} {single} {preview} />
</NodeMarks> </NodeMarks>
{:else} {:else}
<NodeContent {node} {preview} /> <NodeContent {node} {single} {preview} />
{/if} {/if}
{/if} {/if}

View File

@ -22,6 +22,7 @@
import Node from './Node.svelte' import Node from './Node.svelte'
export let node: MarkupNode export let node: MarkupNode
export let single = true
export let preview = false export let preview = false
function toRef (objectId: string): Ref<Doc> { function toRef (objectId: string): Ref<Doc> {
@ -78,11 +79,13 @@
{/if} {/if}
{:else if node.type === MarkupNodeType.text} {:else if node.type === MarkupNodeType.text}
{node.text} {node.text}
{:else if node.type === MarkupNodeType.emoji}
<span class="emoji" class:emojiOnly={single}>{node.attrs?.emoji}</span>
{:else if node.type === MarkupNodeType.paragraph} {:else if node.type === MarkupNodeType.paragraph}
<p class="p-inline contrast" class:overflow-label={preview} class:emojiOnly={checkEmoji(nodes)}> <p class="p-inline contrast" class:overflow-label={preview} class:emojiOnly={checkEmoji(nodes)}>
{#if nodes.length > 0} {#if nodes.length > 0}
{#each nodes as node} {#each nodes as node}
<Node {node} {preview} /> <Node {node} {preview} single={nodes.length === 1} />
{/each} {/each}
{/if} {/if}
</p> </p>

View File

@ -25,6 +25,7 @@ export enum MarkupNodeType {
image = 'image', image = 'image',
file = 'file', file = 'file',
reference = 'reference', reference = 'reference',
emoji = 'emoji',
hard_break = 'hardBreak', hard_break = 'hardBreak',
ordered_list = 'orderedList', ordered_list = 'orderedList',
bullet_list = 'bulletList', bullet_list = 'bulletList',

View File

@ -91,6 +91,7 @@ const nonEmptyNodes = [
MarkupNodeType.horizontal_rule, MarkupNodeType.horizontal_rule,
MarkupNodeType.image, MarkupNodeType.image,
MarkupNodeType.reference, MarkupNodeType.reference,
MarkupNodeType.emoji,
MarkupNodeType.subLink, MarkupNodeType.subLink,
MarkupNodeType.table MarkupNodeType.table
] ]

View File

@ -37,6 +37,7 @@ import { ImageNode, ImageOptions } from '../nodes/image'
import { MarkdownNode } from '../nodes/markdown' import { MarkdownNode } from '../nodes/markdown'
import { MermaidExtension, mermaidOptions } from '../nodes/mermaid' import { MermaidExtension, mermaidOptions } from '../nodes/mermaid'
import { ReferenceNode } from '../nodes/reference' import { ReferenceNode } from '../nodes/reference'
import { EmojiNode } from '../nodes/emoji'
import { TodoItemNode, TodoListNode } from '../nodes/todo' import { TodoItemNode, TodoListNode } from '../nodes/todo'
import { DefaultKit, DefaultKitOptions } from './default-kit' import { DefaultKit, DefaultKitOptions } from './default-kit'
@ -103,6 +104,7 @@ export const ServerKit = Extension.create<ServerKitOptions>({
TodoItemNode, TodoItemNode,
TodoListNode, TodoListNode,
ReferenceNode, ReferenceNode,
EmojiNode,
CommentNode, CommentNode,
MarkdownNode, MarkdownNode,
NodeUuid, NodeUuid,

View File

@ -0,0 +1,79 @@
//
// 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.
//
import { Node, mergeAttributes } from '@tiptap/core'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
emoji: {
insertEmoji: (emoji: string) => ReturnType
}
}
}
export interface EmojiNodeOptions {
emoji: string
}
export const EmojiNode = Node.create<EmojiNodeOptions>({
name: 'emoji',
group: 'inline',
inline: true,
atom: true,
selectable: false,
addAttributes () {
return {
emoji: {
default: ''
}
}
},
addCommands () {
return {
insertEmoji:
(emoji: string) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: { emoji }
})
}
}
},
parseHTML () {
return [
{
tag: `span[data-type="${this.name}"]`
}
]
},
renderHTML ({ node, HTMLAttributes }) {
return [
'span',
mergeAttributes(
{
'data-type': this.name,
class: 'emoji'
},
HTMLAttributes
),
node.attrs.emoji
]
}
})

View File

@ -15,6 +15,7 @@
export * from './image' export * from './image'
export * from './reference' export * from './reference'
export * from './emoji'
export * from './todo' export * from './todo'
export * from './file' export * from './file'
export * from './codeblock' export * from './codeblock'

View File

@ -77,6 +77,9 @@
insertText: (text) => { insertText: (text) => {
editor?.insertText(text) editor?.insertText(text)
}, },
insertEmoji: (emoji) => {
editor?.insertEmoji(emoji)
},
insertMarkup: (markup) => { insertMarkup: (markup) => {
editor?.insertMarkup(markup) editor?.insertMarkup(markup)
}, },

View File

@ -178,6 +178,9 @@
insertText: (text) => { insertText: (text) => {
editor?.commands.insertContent(text) editor?.commands.insertContent(text)
}, },
insertEmoji: (emoji) => {
editor?.commands.insertEmoji(emoji)
},
insertMarkup: (markup) => { insertMarkup: (markup) => {
editor?.commands.insertContent(markupToJSON(markup)) editor?.commands.insertContent(markupToJSON(markup))
}, },

View File

@ -84,6 +84,9 @@
insertText: (text) => { insertText: (text) => {
editor?.insertText(text) editor?.insertText(text)
}, },
insertEmoji: (emoji) => {
editor?.insertEmoji(emoji)
},
insertMarkup: (markup) => { insertMarkup: (markup) => {
editor?.insertMarkup(markup) editor?.insertMarkup(markup)
}, },

View File

@ -79,6 +79,9 @@
insertText: (text) => { insertText: (text) => {
editor?.insertText(text) editor?.insertText(text)
}, },
insertEmoji: (emoji) => {
editor?.insertEmoji(emoji)
},
insertMarkup: (markup) => { insertMarkup: (markup) => {
editor?.insertMarkup(markup) editor?.insertMarkup(markup)
}, },

View File

@ -96,6 +96,10 @@
return editor return editor
} }
export function insertEmoji (emoji: string): void {
editor?.commands.insertEmoji(emoji)
}
export function insertText (text: string): void { export function insertText (text: string): void {
editor?.commands.insertContent(text) editor?.commands.insertContent(text)
} }

View File

@ -27,8 +27,7 @@ export const defaultRefActions: RefAction[] = [
if (emoji === null || emoji === undefined) { if (emoji === null || emoji === undefined) {
return return
} }
editorHandler.insertEmoji(emoji.emoji)
editorHandler.insertText(emoji.emoji)
editorHandler.focus() editorHandler.focus()
}, },
() => {} () => {}

View File

@ -1,4 +1,4 @@
import { Extension } from '@tiptap/core' import { EmojiNode, type EmojiNodeOptions } from '@hcengineering/text'
import { type ResolvedPos } from '@tiptap/pm/model' import { type ResolvedPos } from '@tiptap/pm/model'
const emojiReplaceDict = { const emojiReplaceDict = {
@ -63,7 +63,7 @@ function isValidEmojiPosition ($pos: ResolvedPos): boolean {
return true return true
} }
export const EmojiExtension = Extension.create({ export const EmojiExtension = EmojiNode.extend<EmojiNodeOptions>({
addInputRules () { addInputRules () {
return Object.keys(emojiReplaceDict).map((pattern) => { return Object.keys(emojiReplaceDict).map((pattern) => {
return { return {

View File

@ -16,6 +16,7 @@ export type CollaboratorType = 'local' | 'cloud'
*/ */
export interface TextEditorHandler { export interface TextEditorHandler {
insertText: (html: string) => void insertText: (html: string) => void
insertEmoji: (emoji: string) => void
insertMarkup: (markup: Markup) => void insertMarkup: (markup: Markup) => void
insertTemplate: (name: string, markup: string) => void insertTemplate: (name: string, markup: string) => void
insertTable: (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) => void insertTable: (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) => void