mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-04 22:47:11 +00:00
Add Emoji/FA Autocompletion (#387)
added emoji/fork-awesome autocompletion added autocompletion e2e test Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Co-authored-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
c8c5569426
commit
c15f0d9900
13 changed files with 279 additions and 38 deletions
|
@ -35,6 +35,7 @@
|
|||
- Images will be loaded via proxy if an image proxy is configured in the backend
|
||||
- Asciinema videos may now be embedded by pasting the URL of one video into a single line
|
||||
- The Toolbar includes an EmojiPicker
|
||||
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
64
cypress/integration/autocompletion.spec.ts
Normal file
64
cypress/integration/autocompletion.spec.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
describe('Autocompletion', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.get('.btn.active.btn-outline-secondary > i.fa-columns')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{ctrl}a', { force: true })
|
||||
.type('{backspace}')
|
||||
})
|
||||
|
||||
describe('normal emoji', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type(':book')
|
||||
.type('{enter}')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ':book:')
|
||||
cy.get('.markdown-body')
|
||||
.should('have.text', '📖')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type(':book')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ':book:')
|
||||
cy.get('.markdown-body')
|
||||
.should('have.text', '📖')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fork-awesome-icon', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type(':facebook')
|
||||
.type('{enter}')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ':fa-facebook:')
|
||||
cy.get('.markdown-body > p > i.fa.fa-facebook')
|
||||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type(':facebook')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ':fa-facebook:')
|
||||
cy.get('.markdown-body > p > i.fa.fa-facebook')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,6 +1,7 @@
|
|||
@import '../../../../node_modules/codemirror/lib/codemirror.css';
|
||||
@import '../../../../node_modules/codemirror/addon/display/fullscreen.css';
|
||||
@import './one-dark.css';
|
||||
@import 'hints';
|
||||
|
||||
.CodeMirror {
|
||||
font-family: "Source Code Pro", "twemoji", Consolas, monaco, monospace;
|
||||
|
@ -9,3 +10,4 @@
|
|||
font-size: 18px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Editor } from 'codemirror'
|
||||
import { Editor, EditorChange } from 'codemirror'
|
||||
import 'codemirror/addon/comment/comment'
|
||||
import 'codemirror/addon/display/autorefresh'
|
||||
import 'codemirror/addon/display/fullscreen'
|
||||
|
@ -10,14 +10,16 @@ import 'codemirror/addon/edit/matchbrackets'
|
|||
import 'codemirror/addon/edit/matchtags'
|
||||
import 'codemirror/addon/fold/foldcode'
|
||||
import 'codemirror/addon/fold/foldgutter'
|
||||
import 'codemirror/addon/hint/show-hint'
|
||||
import 'codemirror/addon/search/match-highlighter'
|
||||
import 'codemirror/addon/selection/active-line'
|
||||
import 'codemirror/keymap/sublime.js'
|
||||
import 'codemirror/mode/gfm/gfm.js'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './editor-window.scss'
|
||||
import { emojiHints, emojiWordRegex, findWordAtCursor } from './hints/emoji'
|
||||
import { defaultKeyMap } from './key-map'
|
||||
import { ToolBar } from './tool-bar/tool-bar'
|
||||
|
||||
|
@ -26,10 +28,28 @@ export interface EditorWindowProps {
|
|||
content: string
|
||||
}
|
||||
|
||||
const hintOptions = {
|
||||
hint: emojiHints,
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: false,
|
||||
alignWithWord: true
|
||||
}
|
||||
|
||||
const onChange = (editor: Editor) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
if (emojiWordRegex.test(searchTerm.text)) {
|
||||
editor.showHint(hintOptions)
|
||||
}
|
||||
}
|
||||
|
||||
export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor, setEditor] = useState<Editor>()
|
||||
|
||||
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
|
||||
onContentChange(value)
|
||||
}, [onContentChange])
|
||||
|
||||
return (
|
||||
<div className={'d-flex flex-column h-100'}>
|
||||
<ToolBar
|
||||
|
@ -67,12 +87,13 @@ export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, con
|
|||
addModeClass: true,
|
||||
autoRefresh: true,
|
||||
// otherCursors: true,
|
||||
placeholder: t('editor.placeholder')
|
||||
placeholder: t('editor.placeholder'),
|
||||
showHint: false,
|
||||
hintOptions: hintOptions
|
||||
}}
|
||||
editorDidMount={mountedEditor => setEditor(mountedEditor)}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
onContentChange(value)
|
||||
}}
|
||||
onBeforeChange={onBeforeChange}
|
||||
onChange={onChange}
|
||||
/></div>
|
||||
)
|
||||
}
|
||||
|
|
32
src/components/editor/editor-window/hints.scss
Normal file
32
src/components/editor/editor-window/hints.scss
Normal file
|
@ -0,0 +1,32 @@
|
|||
.CodeMirror-hints {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
list-style: none;
|
||||
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
|
||||
box-shadow: 2px 3px 5px rgba(0,0,0,.2);
|
||||
border-radius: 3px;
|
||||
border: 1px solid silver;
|
||||
|
||||
background: white;
|
||||
|
||||
max-height: 20em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.CodeMirror-hint {
|
||||
margin: 0;
|
||||
padding: 3px 15px;
|
||||
border-radius: 2px;
|
||||
white-space: pre;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
li.CodeMirror-hint-active {
|
||||
background: #08f;
|
||||
color: white;
|
||||
}
|
75
src/components/editor/editor-window/hints/emoji.ts
Normal file
75
src/components/editor/editor-window/hints/emoji.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import { Data, EmojiData, NimbleEmojiIndex } from 'emoji-mart'
|
||||
import data from 'emoji-mart/data/twitter.json'
|
||||
import { getEmojiIcon, getEmojiShortCode } from '../../../../utils/emoji'
|
||||
import { customEmojis } from '../tool-bar/emoji-picker/emoji-picker'
|
||||
|
||||
interface findWordAtCursorResponse {
|
||||
start: number,
|
||||
end: number,
|
||||
text: string
|
||||
}
|
||||
|
||||
const allowedCharsInEmojiCodeRegex = /(:|\w|-|_|\+)/
|
||||
const emojiIndex = new NimbleEmojiIndex(data as unknown as Data)
|
||||
|
||||
export const emojiWordRegex = /^:((\w|-|_|\+)+)$/
|
||||
|
||||
export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
|
||||
const cursor = editor.getCursor()
|
||||
const line = editor.getLine(cursor.line)
|
||||
let start = cursor.ch
|
||||
let end = cursor.ch
|
||||
while (start && allowedCharsInEmojiCodeRegex.test(line.charAt(start - 1))) {
|
||||
--start
|
||||
}
|
||||
while (end < line.length && allowedCharsInEmojiCodeRegex.test(line.charAt(end))) {
|
||||
++end
|
||||
}
|
||||
|
||||
return {
|
||||
text: line.slice(start, end).toLowerCase(),
|
||||
start: start,
|
||||
end: end
|
||||
}
|
||||
}
|
||||
|
||||
export const emojiHints = (editor: Editor): Promise< Hints| null > => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = emojiWordRegex.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const term = searchResult[1]
|
||||
if (!term) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const search = emojiIndex.search(term, {
|
||||
emojisToShowFilter: () => true,
|
||||
maxResults: 5,
|
||||
include: [],
|
||||
exclude: [],
|
||||
custom: customEmojis as EmojiData[]
|
||||
})
|
||||
const cursor = editor.getCursor()
|
||||
if (!search) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: search.map((emojiData: EmojiData): Hint => ({
|
||||
text: getEmojiShortCode(emojiData),
|
||||
render: (parent: HTMLLIElement) => {
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.innerHTML = `${getEmojiIcon(emojiData)} ${getEmojiShortCode(emojiData)}`
|
||||
parent.appendChild(wrapper)
|
||||
}
|
||||
})),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Data, EmojiData, NimblePicker } from 'emoji-mart'
|
||||
import { CustomEmoji, Data, EmojiData, NimblePicker } from 'emoji-mart'
|
||||
import 'emoji-mart/css/emoji-mart.css'
|
||||
import emojiData from 'emoji-mart/data/twitter.json'
|
||||
import React, { useMemo, useRef } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { useClickAway } from 'react-use'
|
||||
import { ShowIf } from '../../../../common/show-if/show-if'
|
||||
import './emoji-picker.scss'
|
||||
|
@ -13,18 +13,18 @@ export interface EmojiPickerProps {
|
|||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export const customEmojis: CustomEmoji[] = Object.keys(ForkAwesomeIcons).map((name) => ({
|
||||
name: `fa-${name}`,
|
||||
short_names: [`fa-${name.toLowerCase()}`],
|
||||
text: '',
|
||||
emoticons: [],
|
||||
keywords: ['fork awesome'],
|
||||
imageUrl: '/img/forkawesome.png',
|
||||
customCategory: 'ForkAwesome'
|
||||
}))
|
||||
|
||||
export const EmojiPicker: React.FC<EmojiPickerProps> = ({ show, onEmojiSelected, onDismiss }) => {
|
||||
const pickerRef = useRef(null)
|
||||
const customIcons = useMemo(() =>
|
||||
Object.keys(ForkAwesomeIcons).map((name) => ({
|
||||
name: `fa-${name}`,
|
||||
short_names: [`fa-${name.toLowerCase()}`],
|
||||
text: '',
|
||||
emoticons: [],
|
||||
keywords: ['fork awesome'],
|
||||
imageUrl: '/img/forkawesome.png',
|
||||
customCategory: 'ForkAwesome'
|
||||
})), [])
|
||||
|
||||
useClickAway(pickerRef, () => {
|
||||
onDismiss()
|
||||
|
@ -39,7 +39,7 @@ export const EmojiPicker: React.FC<EmojiPickerProps> = ({ show, onEmojiSelected,
|
|||
onSelect={onEmojiSelected}
|
||||
theme={'auto'}
|
||||
title=''
|
||||
custom={customIcons}
|
||||
custom={customEmojis}
|
||||
/>
|
||||
</div>
|
||||
</ShowIf>
|
||||
|
|
|
@ -1738,10 +1738,10 @@ describe('test addEmoji with native emoji', () => {
|
|||
describe('test addEmoji with native emoji', () => {
|
||||
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
||||
const textFirstLine = testContent.split('\n')[0]
|
||||
// noinspection CheckTagEmptyBody
|
||||
const forkAwesomeIcon = '<i class="fa star"></i>'
|
||||
const forkAwesomeIcon = ':fa-star:'
|
||||
const emoji = Mock.of<EmojiData>({
|
||||
name: 'star',
|
||||
colons: ':fa-star:',
|
||||
imageUrl: '/img/forkawesome.png'
|
||||
})
|
||||
it('just cursor', done => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Editor } from 'codemirror'
|
||||
import { BaseEmoji, CustomEmoji, EmojiData } from 'emoji-mart'
|
||||
import { EmojiData } from 'emoji-mart'
|
||||
import { getEmojiShortCode } from '../../../../utils/emoji'
|
||||
|
||||
export const makeSelectionBold = (editor: Editor): void => wrapTextWith(editor, '**')
|
||||
export const makeSelectionItalic = (editor: Editor): void => wrapTextWith(editor, '*')
|
||||
|
@ -24,14 +25,7 @@ export const addComment = (editor: Editor): void => changeLines(editor, line =>
|
|||
export const addTable = (editor: Editor): void => changeLines(editor, line => `${line}\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |`)
|
||||
|
||||
export const addEmoji = (emoji: EmojiData, editor: Editor): void => {
|
||||
let replacement = ''
|
||||
if ((emoji as BaseEmoji).colons) {
|
||||
replacement = (emoji as BaseEmoji).colons
|
||||
} else if ((emoji as CustomEmoji).imageUrl) {
|
||||
// noinspection CheckTagEmptyBody
|
||||
replacement = `<i class="fa ${(emoji as CustomEmoji).name}"></i>`
|
||||
}
|
||||
insertAtCursor(editor, replacement)
|
||||
insertAtCursor(editor, getEmojiShortCode(emoji))
|
||||
}
|
||||
|
||||
export const wrapTextWith = (editor: Editor, symbol: string, endSymbol?: string): void => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import equal from 'deep-equal'
|
||||
import { DomElement } from 'domhandler'
|
||||
import emojiData from 'emoji-mart/data/twitter.json'
|
||||
import { Data } from 'emoji-mart/dist-es/utils/data'
|
||||
import yaml from 'js-yaml'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
@ -14,8 +15,8 @@ import imsize from 'markdown-it-imsize'
|
|||
import inserted from 'markdown-it-ins'
|
||||
import marked from 'markdown-it-mark'
|
||||
import mathJax from 'markdown-it-mathjax'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import plantuml from 'markdown-it-plantuml'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import subscript from 'markdown-it-sub'
|
||||
import superscript from 'markdown-it-sup'
|
||||
import taskList from 'markdown-it-task-lists'
|
||||
|
@ -31,13 +32,14 @@ import { ApplicationState } from '../../../redux'
|
|||
import { slugify } from '../../../utils/slugify'
|
||||
import { InternalLink } from '../../common/links/internal-link'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { ForkAwesomeIcons } from '../editor-window/tool-bar/emoji-picker/icon-names'
|
||||
import { RawYAMLMetadata, YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||
import { createRenderContainer, validAlertLevels } from './container-plugins/alert'
|
||||
import { highlightedCode } from './markdown-it-plugins/highlighted-code'
|
||||
import { linkifyExtra } from './markdown-it-plugins/linkify-extra'
|
||||
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
|
||||
import './markdown-renderer.scss'
|
||||
import { plantumlError } from './markdown-it-plugins/plantuml-error'
|
||||
import './markdown-renderer.scss'
|
||||
import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link'
|
||||
import { replaceGistLink } from './regex-plugins/replace-gist-link'
|
||||
import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code'
|
||||
|
@ -51,8 +53,8 @@ import { replaceQuoteExtraColor } from './regex-plugins/replace-quote-extra-colo
|
|||
import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time'
|
||||
import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
|
||||
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
|
||||
import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer'
|
||||
import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
|
||||
import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer'
|
||||
import { GistReplacer } from './replace-components/gist/gist-replacer'
|
||||
import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer'
|
||||
import { ImageReplacer } from './replace-components/image/image-replacer'
|
||||
|
@ -63,7 +65,6 @@ import { QuoteOptionsReplacer } from './replace-components/quote-options/quote-o
|
|||
import { TocReplacer } from './replace-components/toc/toc-replacer'
|
||||
import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
|
||||
import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
|
||||
import emojiData from 'emoji-mart/data/twitter.json'
|
||||
|
||||
export interface MarkdownRendererProps {
|
||||
content: string
|
||||
|
@ -77,12 +78,29 @@ export interface MarkdownRendererProps {
|
|||
const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
|
||||
.reduce((reduceObject, emojiIdentifier) => {
|
||||
const emoji = (emojiData as unknown as Data).emojis[emojiIdentifier]
|
||||
if (emoji.b) {
|
||||
reduceObject[emojiIdentifier] = `&#x${emoji.b};`
|
||||
if (emoji.unified) {
|
||||
reduceObject[emojiIdentifier] = emoji.unified.split('-').map(char => `&#x${char};`).join('')
|
||||
}
|
||||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
const emojiSkinToneModifierMap = [2, 3, 4, 5, 6]
|
||||
.reduce((reduceObject, modifierValue) => {
|
||||
const lightSkinCode = 127995
|
||||
const codepoint = lightSkinCode + (modifierValue - 2)
|
||||
const shortcode = `skin-tone-${modifierValue}`
|
||||
reduceObject[shortcode] = `&#${codepoint};`
|
||||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
|
||||
.reduce((reduceObject, icon) => {
|
||||
const shortcode = `fa-${icon}`
|
||||
// noinspection CheckTagEmptyBody
|
||||
reduceObject[shortcode] = `<i class="fa fa-${icon}"></i>`
|
||||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide }) => {
|
||||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
const [lastTocAst, setLastTocAst] = useState<TocAst>()
|
||||
|
@ -154,7 +172,11 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
|
|||
md.use(plantumlError)
|
||||
}
|
||||
md.use(emoji, {
|
||||
defs: markdownItTwitterEmojis
|
||||
defs: {
|
||||
...markdownItTwitterEmojis,
|
||||
...emojiSkinToneModifierMap,
|
||||
...forkAwesomeIconMap
|
||||
}
|
||||
})
|
||||
md.use(abbreviation)
|
||||
md.use(definitionList)
|
||||
|
|
15
src/external-types/emoji-mart/dist-es/utils/emoji-index/nimble-emoji-index.d.ts
vendored
Normal file
15
src/external-types/emoji-mart/dist-es/utils/emoji-index/nimble-emoji-index.d.ts
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
import 'emoji-mart'
|
||||
|
||||
declare module 'emoji-mart' {
|
||||
export interface SearchOption {
|
||||
emojisToShowFilter: (emoji: EmojiData) => boolean
|
||||
maxResults: number,
|
||||
include: EmojiData[]
|
||||
exclude: EmojiData[]
|
||||
custom: EmojiData[]
|
||||
}
|
||||
|
||||
export class NimbleEmojiIndex {
|
||||
search (query: string, options: SearchOption): EmojiData[] | null;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ html {
|
|||
body {
|
||||
min-height: 100%;
|
||||
background-color: darken($dark, 8%);
|
||||
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
|
||||
font-family: "Source Sans Pro", Helvetica, Arial, twemoji, sans-serif;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
|
|
15
src/utils/emoji.ts
Normal file
15
src/utils/emoji.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { BaseEmoji, CustomEmoji, EmojiData } from 'emoji-mart'
|
||||
|
||||
export const getEmojiIcon = (emoji: EmojiData):string => {
|
||||
if ((emoji as BaseEmoji).native) {
|
||||
return (emoji as BaseEmoji).native
|
||||
} else if ((emoji as CustomEmoji).imageUrl) {
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<i class="fa ${(emoji as CustomEmoji).name}"></i>`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export const getEmojiShortCode = (emoji: EmojiData):string => {
|
||||
return (emoji as BaseEmoji).colons
|
||||
}
|
Loading…
Reference in a new issue