From 4d1b2a436d94be01274b4c2fb38f0925b1e7b9b9 Mon Sep 17 00:00:00 2001 From: Anton Alexeyev Date: Tue, 29 Apr 2025 15:29:03 +0700 Subject: [PATCH] Add emoji text node Signed-off-by: Anton Alexeyev --- .../src/components/markup/Node.svelte | 5 +- .../src/components/markup/NodeContent.svelte | 5 +- packages/text-core/src/markup/model.ts | 1 + packages/text-core/src/markup/utils.ts | 1 + packages/text/src/kits/server-kit.ts | 2 + packages/text/src/nodes/emoji.ts | 79 +++++++++++++++++++ packages/text/src/nodes/index.ts | 1 + .../ui-next/src/components/TextInput.svelte | 3 + .../components/CollaborativeTextEditor.svelte | 3 + .../src/components/ReferenceInput.svelte | 3 + .../src/components/StyledTextEditor.svelte | 3 + .../src/components/TextEditor.svelte | 4 + .../src/components/editor/actions.ts | 3 +- .../src/components/extension/emoji.ts | 4 +- plugins/text-editor/src/types.ts | 1 + 15 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 packages/text/src/nodes/emoji.ts diff --git a/packages/presentation/src/components/markup/Node.svelte b/packages/presentation/src/components/markup/Node.svelte index 66e0d20b0f..9344d1da03 100644 --- a/packages/presentation/src/components/markup/Node.svelte +++ b/packages/presentation/src/components/markup/Node.svelte @@ -19,6 +19,7 @@ import NodeContent from './NodeContent.svelte' export let node: MarkupNode + export let single = true export let preview = false @@ -27,9 +28,9 @@ {#if marks.length > 0} - + {:else} - + {/if} {/if} diff --git a/packages/presentation/src/components/markup/NodeContent.svelte b/packages/presentation/src/components/markup/NodeContent.svelte index 9cf1357914..ca46d66037 100644 --- a/packages/presentation/src/components/markup/NodeContent.svelte +++ b/packages/presentation/src/components/markup/NodeContent.svelte @@ -22,6 +22,7 @@ import Node from './Node.svelte' export let node: MarkupNode + export let single = true export let preview = false function toRef (objectId: string): Ref { @@ -78,11 +79,13 @@ {/if} {:else if node.type === MarkupNodeType.text} {node.text} + {:else if node.type === MarkupNodeType.emoji} + {node.attrs?.emoji} {:else if node.type === MarkupNodeType.paragraph}

{#if nodes.length > 0} {#each nodes as node} - + {/each} {/if}

diff --git a/packages/text-core/src/markup/model.ts b/packages/text-core/src/markup/model.ts index ef5878cd22..fb8f7802a9 100644 --- a/packages/text-core/src/markup/model.ts +++ b/packages/text-core/src/markup/model.ts @@ -25,6 +25,7 @@ export enum MarkupNodeType { image = 'image', file = 'file', reference = 'reference', + emoji = 'emoji', hard_break = 'hardBreak', ordered_list = 'orderedList', bullet_list = 'bulletList', diff --git a/packages/text-core/src/markup/utils.ts b/packages/text-core/src/markup/utils.ts index f19e154831..7b187aac46 100644 --- a/packages/text-core/src/markup/utils.ts +++ b/packages/text-core/src/markup/utils.ts @@ -91,6 +91,7 @@ const nonEmptyNodes = [ MarkupNodeType.horizontal_rule, MarkupNodeType.image, MarkupNodeType.reference, + MarkupNodeType.emoji, MarkupNodeType.subLink, MarkupNodeType.table ] diff --git a/packages/text/src/kits/server-kit.ts b/packages/text/src/kits/server-kit.ts index 0326eaaa92..96b69abf83 100644 --- a/packages/text/src/kits/server-kit.ts +++ b/packages/text/src/kits/server-kit.ts @@ -37,6 +37,7 @@ import { ImageNode, ImageOptions } from '../nodes/image' import { MarkdownNode } from '../nodes/markdown' import { MermaidExtension, mermaidOptions } from '../nodes/mermaid' import { ReferenceNode } from '../nodes/reference' +import { EmojiNode } from '../nodes/emoji' import { TodoItemNode, TodoListNode } from '../nodes/todo' import { DefaultKit, DefaultKitOptions } from './default-kit' @@ -103,6 +104,7 @@ export const ServerKit = Extension.create({ TodoItemNode, TodoListNode, ReferenceNode, + EmojiNode, CommentNode, MarkdownNode, NodeUuid, diff --git a/packages/text/src/nodes/emoji.ts b/packages/text/src/nodes/emoji.ts new file mode 100644 index 0000000000..2e11bc4464 --- /dev/null +++ b/packages/text/src/nodes/emoji.ts @@ -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 { + emoji: { + insertEmoji: (emoji: string) => ReturnType + } + } +} + +export interface EmojiNodeOptions { + emoji: string +} + +export const EmojiNode = Node.create({ + 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 + ] + } +}) diff --git a/packages/text/src/nodes/index.ts b/packages/text/src/nodes/index.ts index e66f91c757..09a0ba37bf 100644 --- a/packages/text/src/nodes/index.ts +++ b/packages/text/src/nodes/index.ts @@ -15,6 +15,7 @@ export * from './image' export * from './reference' +export * from './emoji' export * from './todo' export * from './file' export * from './codeblock' diff --git a/packages/ui-next/src/components/TextInput.svelte b/packages/ui-next/src/components/TextInput.svelte index 9777075985..7b4dfebf72 100644 --- a/packages/ui-next/src/components/TextInput.svelte +++ b/packages/ui-next/src/components/TextInput.svelte @@ -77,6 +77,9 @@ insertText: (text) => { editor?.insertText(text) }, + insertEmoji: (emoji) => { + editor?.insertEmoji(emoji) + }, insertMarkup: (markup) => { editor?.insertMarkup(markup) }, diff --git a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte index 6e86274010..2576c0e3e7 100644 --- a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte +++ b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte @@ -178,6 +178,9 @@ insertText: (text) => { editor?.commands.insertContent(text) }, + insertEmoji: (emoji) => { + editor?.commands.insertEmoji(emoji) + }, insertMarkup: (markup) => { editor?.commands.insertContent(markupToJSON(markup)) }, diff --git a/plugins/text-editor-resources/src/components/ReferenceInput.svelte b/plugins/text-editor-resources/src/components/ReferenceInput.svelte index 2ee2729f2f..e73bd8003e 100644 --- a/plugins/text-editor-resources/src/components/ReferenceInput.svelte +++ b/plugins/text-editor-resources/src/components/ReferenceInput.svelte @@ -84,6 +84,9 @@ insertText: (text) => { editor?.insertText(text) }, + insertEmoji: (emoji) => { + editor?.insertEmoji(emoji) + }, insertMarkup: (markup) => { editor?.insertMarkup(markup) }, diff --git a/plugins/text-editor-resources/src/components/StyledTextEditor.svelte b/plugins/text-editor-resources/src/components/StyledTextEditor.svelte index 9c760e903a..7831e59cc2 100644 --- a/plugins/text-editor-resources/src/components/StyledTextEditor.svelte +++ b/plugins/text-editor-resources/src/components/StyledTextEditor.svelte @@ -79,6 +79,9 @@ insertText: (text) => { editor?.insertText(text) }, + insertEmoji: (emoji) => { + editor?.insertEmoji(emoji) + }, insertMarkup: (markup) => { editor?.insertMarkup(markup) }, diff --git a/plugins/text-editor-resources/src/components/TextEditor.svelte b/plugins/text-editor-resources/src/components/TextEditor.svelte index a0dc4d2917..9e8984851b 100644 --- a/plugins/text-editor-resources/src/components/TextEditor.svelte +++ b/plugins/text-editor-resources/src/components/TextEditor.svelte @@ -96,6 +96,10 @@ return editor } + export function insertEmoji (emoji: string): void { + editor?.commands.insertEmoji(emoji) + } + export function insertText (text: string): void { editor?.commands.insertContent(text) } diff --git a/plugins/text-editor-resources/src/components/editor/actions.ts b/plugins/text-editor-resources/src/components/editor/actions.ts index 39e2d6b199..e76b17e1da 100644 --- a/plugins/text-editor-resources/src/components/editor/actions.ts +++ b/plugins/text-editor-resources/src/components/editor/actions.ts @@ -27,8 +27,7 @@ export const defaultRefActions: RefAction[] = [ if (emoji === null || emoji === undefined) { return } - - editorHandler.insertText(emoji.emoji) + editorHandler.insertEmoji(emoji.emoji) editorHandler.focus() }, () => {} diff --git a/plugins/text-editor-resources/src/components/extension/emoji.ts b/plugins/text-editor-resources/src/components/extension/emoji.ts index a18c684141..55d6825d19 100644 --- a/plugins/text-editor-resources/src/components/extension/emoji.ts +++ b/plugins/text-editor-resources/src/components/extension/emoji.ts @@ -1,4 +1,4 @@ -import { Extension } from '@tiptap/core' +import { EmojiNode, type EmojiNodeOptions } from '@hcengineering/text' import { type ResolvedPos } from '@tiptap/pm/model' const emojiReplaceDict = { @@ -63,7 +63,7 @@ function isValidEmojiPosition ($pos: ResolvedPos): boolean { return true } -export const EmojiExtension = Extension.create({ +export const EmojiExtension = EmojiNode.extend({ addInputRules () { return Object.keys(emojiReplaceDict).map((pattern) => { return { diff --git a/plugins/text-editor/src/types.ts b/plugins/text-editor/src/types.ts index 62f97dfdc1..43074d6298 100644 --- a/plugins/text-editor/src/types.ts +++ b/plugins/text-editor/src/types.ts @@ -16,6 +16,7 @@ export type CollaboratorType = 'local' | 'cloud' */ export interface TextEditorHandler { insertText: (html: string) => void + insertEmoji: (emoji: string) => void insertMarkup: (markup: Markup) => void insertTemplate: (name: string, markup: string) => void insertTable: (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) => void