kopia lustrzana https://github.com/elk-zone/elk
				
				
				
			feat: custom emoji font in editor
							rodzic
							
								
									e97068f000
								
							
						
					
					
						commit
						8f68fa12e4
					
				|  | @ -65,11 +65,11 @@ async function handlePaste(evt: ClipboardEvent) { | |||
|   await uploadAttachments(Array.from(files)) | ||||
| } | ||||
| 
 | ||||
| function insertText(text: string) { | ||||
|   editor.value?.chain().insertContent(text).focus().run() | ||||
| function insertEmoji(name: string) { | ||||
|   editor.value?.chain().focus().insertEmoji(name).run() | ||||
| } | ||||
| function insertEmoji(image: any) { | ||||
|   editor.value?.chain().focus().setEmoji(image).run() | ||||
| function insertCustomEmoji(image: any) { | ||||
|   editor.value?.chain().focus().insertCustomEmoji(image).run() | ||||
| } | ||||
| 
 | ||||
| async function pickAttachments() { | ||||
|  | @ -196,7 +196,7 @@ defineExpose({ | |||
|       <div border="b dashed gray/40" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <div flex gap-4 flex-1> | ||||
|     <div flex gap-3 flex-1> | ||||
|       <NuxtLink :to="getAccountRoute(currentUser.account)"> | ||||
|         <AccountAvatar :account="currentUser.account" account-avatar-normal /> | ||||
|       </NuxtLink> | ||||
|  | @ -280,7 +280,10 @@ defineExpose({ | |||
|         v-if="shouldExpanded" flex="~ gap-2 1" m="l--1" pt-2 justify="between" max-full | ||||
|         border="t base" | ||||
|       > | ||||
|         <PublishEmojiPicker @select="insertText" @select-custom="insertEmoji" /> | ||||
|         <PublishEmojiPicker | ||||
|           @select="insertEmoji" | ||||
|           @select-custom="insertCustomEmoji" | ||||
|         /> | ||||
| 
 | ||||
|         <CommonTooltip placement="bottom" :content="$t('tooltip.add_media')"> | ||||
|           <button btn-action-icon :aria-label="$t('tooltip.add_media')" @click="pickAttachments"> | ||||
|  |  | |||
|  | @ -1,8 +1,9 @@ | |||
| // @unimport-disable
 | ||||
| import type { Emoji } from 'masto' | ||||
| import type { Node } from 'ultrahtml' | ||||
| import { TEXT_NODE, parse, render, walkSync } from 'ultrahtml' | ||||
| 
 | ||||
| const EMOJI_REGEX = /(\p{Emoji_Presentation})/ug | ||||
| export const EMOJI_REGEX = /(\p{Emoji_Presentation})/ug | ||||
| 
 | ||||
| const decoder = process.client ? document.createElement('textarea') : null as any as HTMLTextAreaElement | ||||
| export function decodeHtml(text: string) { | ||||
|  | @ -118,7 +119,10 @@ export function treeToText(input: Node): string { | |||
| 
 | ||||
|   // add spaces around emoji to prevent parsing errors: 2 or more consecutive emojis will not be parsed
 | ||||
|   if (input.name === 'img' && input.attributes.class?.includes('custom-emoji')) | ||||
|     return ` :${input.attributes['data-emoji-id']}: ` | ||||
|     return `:${input.attributes['data-emoji-id']}:` | ||||
| 
 | ||||
|   if (input.name === 'em-emoji') | ||||
|     return `${input.attributes.native}` | ||||
| 
 | ||||
|   return pre + body + post | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import type { Emoji } from 'masto' | ||||
| import type { CustomEmojisInfo } from './push-notifications/types' | ||||
| import { STORAGE_KEY_CUSTOM_EMOJIS } from '~/constants' | ||||
| import { useUserLocalStorage } from '~/composables/users' | ||||
| 
 | ||||
| const TTL = 1000 * 60 * 60 * 24 // 1 day
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ import { Plugin } from 'prosemirror-state' | |||
| import type { Ref } from 'vue' | ||||
| import { HashSuggestion, MentionSuggestion } from './tiptap/suggestion' | ||||
| import { CodeBlockShiki } from './tiptap/shiki' | ||||
| import { CustomEmoji } from './tiptap/custom-emoji' | ||||
| import { Emoji } from './tiptap/emoji' | ||||
| 
 | ||||
| export interface UseTiptapOptions { | ||||
|  | @ -43,7 +44,7 @@ export function useTiptap(options: UseTiptapOptions) { | |||
|       Code, | ||||
|       Text, | ||||
|       Emoji, | ||||
|       Emoji.configure({ | ||||
|       CustomEmoji.configure({ | ||||
|         inline: true, | ||||
|         HTMLAttributes: { | ||||
|           class: 'custom-emoji', | ||||
|  |  | |||
|  | @ -0,0 +1,112 @@ | |||
| import { | ||||
|   Node, | ||||
|   mergeAttributes, | ||||
|   nodeInputRule, | ||||
| } from '@tiptap/core' | ||||
| 
 | ||||
| export interface EmojiOptions { | ||||
|   inline: boolean | ||||
|   allowBase64: boolean | ||||
|   HTMLAttributes: Record<string, any> | ||||
| } | ||||
| 
 | ||||
| declare module '@tiptap/core' { | ||||
|   interface Commands<ReturnType> { | ||||
|     emoji: { | ||||
|       /** | ||||
|        * Insert a custom emoji. | ||||
|        */ | ||||
|       insertCustomEmoji: (options: { src: string; alt?: string; title?: string }) => ReturnType | ||||
|       /** | ||||
|        * Insert a emoji. | ||||
|        */ | ||||
|       insertEmoji: (native: string) => ReturnType | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/ | ||||
| 
 | ||||
| export const CustomEmoji = Node.create<EmojiOptions>({ | ||||
|   name: 'custom-emoji', | ||||
| 
 | ||||
|   addOptions() { | ||||
|     return { | ||||
|       inline: false, | ||||
|       allowBase64: false, | ||||
|       HTMLAttributes: {}, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   inline() { | ||||
|     return this.options.inline | ||||
|   }, | ||||
| 
 | ||||
|   group() { | ||||
|     return this.options.inline ? 'inline' : 'block' | ||||
|   }, | ||||
| 
 | ||||
|   draggable: false, | ||||
| 
 | ||||
|   addAttributes() { | ||||
|     return { | ||||
|       'src': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'alt': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'title': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'width': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'height': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'data-emoji-id': { | ||||
|         default: null, | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   parseHTML() { | ||||
|     return [ | ||||
|       { | ||||
|         tag: this.options.allowBase64 | ||||
|           ? 'img[src]' | ||||
|           : 'img[src]:not([src^="data:"])', | ||||
|       }, | ||||
|     ] | ||||
|   }, | ||||
| 
 | ||||
|   renderHTML({ HTMLAttributes }) { | ||||
|     return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)] | ||||
|   }, | ||||
| 
 | ||||
|   addCommands() { | ||||
|     return { | ||||
|       insertCustomEmoji: options => ({ commands }) => { | ||||
|         return commands.insertContent({ | ||||
|           type: this.name, | ||||
|           attrs: options, | ||||
|         }) | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   addInputRules() { | ||||
|     return [ | ||||
|       nodeInputRule({ | ||||
|         find: inputRegex, | ||||
|         type: this.type, | ||||
|         getAttributes: (match) => { | ||||
|           const [,, alt, src, title] = match | ||||
| 
 | ||||
|           return { src, alt, title } | ||||
|         }, | ||||
|       }), | ||||
|     ] | ||||
|   }, | ||||
| }) | ||||
|  | @ -4,89 +4,41 @@ import { | |||
|   nodeInputRule, | ||||
| } from '@tiptap/core' | ||||
| 
 | ||||
| export interface EmojiOptions { | ||||
|   inline: boolean | ||||
|   allowBase64: boolean | ||||
|   HTMLAttributes: Record<string, any> | ||||
| } | ||||
| 
 | ||||
| declare module '@tiptap/core' { | ||||
|   interface Commands<ReturnType> { | ||||
|     emoji: { | ||||
|       /** | ||||
|        * Add an emoji. | ||||
|        */ | ||||
|       setEmoji: (options: { src: string; alt?: string; title?: string }) => ReturnType | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/ | ||||
| 
 | ||||
| export const Emoji = Node.create<EmojiOptions>({ | ||||
|   name: 'custom-emoji', | ||||
| 
 | ||||
|   addOptions() { | ||||
|     return { | ||||
|       inline: false, | ||||
|       allowBase64: false, | ||||
|       HTMLAttributes: {}, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   inline() { | ||||
|     return this.options.inline | ||||
|   }, | ||||
| 
 | ||||
|   group() { | ||||
|     return this.options.inline ? 'inline' : 'block' | ||||
|   }, | ||||
| export const Emoji = Node.create({ | ||||
|   name: 'em-emoji', | ||||
| 
 | ||||
|   inline: () => true, | ||||
|   group: () => 'inline', | ||||
|   draggable: false, | ||||
| 
 | ||||
|   addAttributes() { | ||||
|     return { | ||||
|       'src': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'alt': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'title': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'width': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'height': { | ||||
|         default: null, | ||||
|       }, | ||||
|       'data-emoji-id': { | ||||
|         default: null, | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   parseHTML() { | ||||
|     return [ | ||||
|       { | ||||
|         tag: this.options.allowBase64 | ||||
|           ? 'img[src]' | ||||
|           : 'img[src]:not([src^="data:"])', | ||||
|         tag: 'em-emoji[native]', | ||||
|       }, | ||||
|     ] | ||||
|   }, | ||||
| 
 | ||||
|   renderHTML({ HTMLAttributes }) { | ||||
|     return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)] | ||||
|   addAttributes() { | ||||
|     return { | ||||
|       native: { | ||||
|         default: null, | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   renderHTML(args) { | ||||
|     return ['em-emoji', mergeAttributes(this.options.HTMLAttributes, args.HTMLAttributes)] | ||||
|   }, | ||||
| 
 | ||||
|   addCommands() { | ||||
|     return { | ||||
|       setEmoji: options => ({ commands }) => { | ||||
|       insertEmoji: name => ({ commands }) => { | ||||
|         return commands.insertContent({ | ||||
|           type: this.name, | ||||
|           attrs: options, | ||||
|           attrs: { | ||||
|             native: name, | ||||
|           }, | ||||
|         }) | ||||
|       }, | ||||
|     } | ||||
|  | @ -95,12 +47,11 @@ export const Emoji = Node.create<EmojiOptions>({ | |||
|   addInputRules() { | ||||
|     return [ | ||||
|       nodeInputRule({ | ||||
|         find: inputRegex, | ||||
|         find: EMOJI_REGEX, | ||||
|         type: this.type, | ||||
|         getAttributes: (match) => { | ||||
|           const [,, alt, src, title] = match | ||||
| 
 | ||||
|           return { src, alt, title } | ||||
|           const [native] = match | ||||
|           return { native } | ||||
|         }, | ||||
|       }), | ||||
|     ] | ||||
|  |  | |||
|  | @ -85,6 +85,7 @@ body { | |||
|   overflow: hidden; | ||||
|   max-height: 1.3em; | ||||
|   max-width: 1.3em; | ||||
|   margin: 0 0.2em; | ||||
|   vertical-align: text-bottom; | ||||
| } | ||||
| 
 | ||||
|  | @ -133,12 +134,6 @@ body { | |||
|   max-width: 100%; | ||||
| } | ||||
| 
 | ||||
| .content-editor.content-rich { | ||||
|   p { | ||||
|     --at-apply: my-0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .skeleton-loading-bg { | ||||
|   background: linear-gradient( | ||||
|     90deg, | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Anthony Fu
						Anthony Fu