mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-17 08:34:54 -04:00
Replace emoji-mart with emoji-picker-element (#620)
* Change dependencies * Use emoji-picker-element instead of emoji-mart * Optimize emoji-picker appeareance and data-source * Add twemoji font to emoji-picker * Add missing useEffect dependency * Add emoji-shortcode map * Include emoji-data into bundle and remove dynamic fetch * Rename shortcode-map * Fix emoji-picker being hidden on second attempt to open it * Add support for skin-tone short-codes * Remove whitespace line * Don't reinitialize the picker on every open * Fixed linting and test issues * Update CHANGELOG entry
This commit is contained in:
parent
fe40d7247d
commit
5574f09ef5
15 changed files with 203 additions and 167 deletions
|
@ -35,7 +35,7 @@
|
||||||
- HedgeDoc instances can now be branded either with a '@ <custom string>' or '@ <custom logo>' after the HedgeDoc logo and text
|
- HedgeDoc instances can now be branded either with a '@ <custom string>' or '@ <custom logo>' after the HedgeDoc logo and text
|
||||||
- Images will be loaded via proxy if an image proxy is configured in the backend
|
- 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
|
- Asciinema videos may now be embedded by pasting the URL of one video into a single line
|
||||||
- The toolbar includes an EmojiPicker
|
- The toolbar includes an emoji and fork-awesome icon picker.
|
||||||
- Collapsable blocks can be added via a toolbar button or via autocompletion of "<details"
|
- Collapsable blocks can be added via a toolbar button or via autocompletion of "<details"
|
||||||
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
|
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
|
||||||
- The code button now adds code fences even if the user selected nothing beforehand
|
- The code button now adds code fences even if the user selected nothing beforehand
|
||||||
|
|
|
@ -8,7 +8,8 @@ module.exports = {
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
{ from: 'node_modules/@hpcc-js/wasm/dist/graphvizlib.wasm', to: 'static/js' },
|
{ from: 'node_modules/@hpcc-js/wasm/dist/graphvizlib.wasm', to: 'static/js' },
|
||||||
{ from: 'node_modules/@hpcc-js/wasm/dist/expatlib.wasm', to: 'static/js' }
|
{ from: 'node_modules/@hpcc-js/wasm/dist/expatlib.wasm', to: 'static/js' },
|
||||||
|
{ from: 'node_modules/emojibase-data/en/data.json', to: 'static/js/emoji-data.json' }
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
...when(Boolean(process.env.ANALYZE), () => [
|
...when(Boolean(process.env.ANALYZE), () => [
|
||||||
|
|
|
@ -80,7 +80,7 @@ describe('Autocompletion', () => {
|
||||||
describe('normal emoji', () => {
|
describe('normal emoji', () => {
|
||||||
it('via Enter', () => {
|
it('via Enter', () => {
|
||||||
cy.get('.CodeMirror textarea')
|
cy.get('.CodeMirror textarea')
|
||||||
.type(':book')
|
.type(':hedg')
|
||||||
cy.get('.CodeMirror-hints')
|
cy.get('.CodeMirror-hints')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('.CodeMirror textarea')
|
cy.get('.CodeMirror textarea')
|
||||||
|
@ -88,29 +88,29 @@ describe('Autocompletion', () => {
|
||||||
cy.get('.CodeMirror-hints')
|
cy.get('.CodeMirror-hints')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', ':book:')
|
.should('have.text', ':hedgehog:')
|
||||||
cy.get('.markdown-body')
|
cy.get('.markdown-body')
|
||||||
.should('have.text', '📖')
|
.should('have.text', '🦔')
|
||||||
})
|
})
|
||||||
it('via doubleclick', () => {
|
it('via doubleclick', () => {
|
||||||
cy.get('.CodeMirror textarea')
|
cy.get('.CodeMirror textarea')
|
||||||
.type(':book')
|
.type(':hedg')
|
||||||
cy.get('.CodeMirror-hints > li')
|
cy.get('.CodeMirror-hints > li')
|
||||||
.first()
|
.first()
|
||||||
.dblclick()
|
.dblclick()
|
||||||
cy.get('.CodeMirror-hints')
|
cy.get('.CodeMirror-hints')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', ':book:')
|
.should('have.text', ':hedgehog:')
|
||||||
cy.get('.markdown-body')
|
cy.get('.markdown-body')
|
||||||
.should('have.text', '📖')
|
.should('have.text', '🦔')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('fork-awesome-icon', () => {
|
describe('fork-awesome-icon', () => {
|
||||||
it('via Enter', () => {
|
it('via Enter', () => {
|
||||||
cy.get('.CodeMirror textarea')
|
cy.get('.CodeMirror textarea')
|
||||||
.type(':facebook')
|
.type(':fa-face')
|
||||||
cy.get('.CodeMirror-hints')
|
cy.get('.CodeMirror-hints')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('.CodeMirror textarea')
|
cy.get('.CodeMirror textarea')
|
||||||
|
@ -124,7 +124,7 @@ describe('Autocompletion', () => {
|
||||||
})
|
})
|
||||||
it('via doubleclick', () => {
|
it('via doubleclick', () => {
|
||||||
cy.get('.CodeMirror textarea')
|
cy.get('.CodeMirror textarea')
|
||||||
.type(':facebook')
|
.type(':fa-face')
|
||||||
cy.get('.CodeMirror-hints > li')
|
cy.get('.CodeMirror-hints > li')
|
||||||
.first()
|
.first()
|
||||||
.dblclick()
|
.dblclick()
|
||||||
|
|
|
@ -275,28 +275,14 @@ describe('Toolbar', () => {
|
||||||
.should('have.text', '> []')
|
.should('have.text', '> []')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('emoji', () => {
|
describe('emoji-picker', () => {
|
||||||
it('picker is show when clicked', () => {
|
it('show when clicked', () => {
|
||||||
cy.get('.emoji-mart')
|
cy.get('emoji-picker')
|
||||||
.should('not.exist')
|
.should('not.be.visible')
|
||||||
cy.get('.fa-smile-o')
|
cy.get('.fa-smile-o')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.emoji-mart')
|
cy.get('emoji-picker')
|
||||||
.should('exist')
|
.should('be.visible')
|
||||||
})
|
|
||||||
|
|
||||||
it('picker is show when clicked', () => {
|
|
||||||
cy.get('.fa-smile-o')
|
|
||||||
.click()
|
|
||||||
cy.get('.emoji-mart')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('.emoji-mart-emoji-native')
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
cy.get('.markdown-body')
|
|
||||||
.should('have.text', '👍')
|
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span ')
|
|
||||||
.should('have.text', ':+1:')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
"@types/d3-graphviz": "2.6.6",
|
"@types/d3-graphviz": "2.6.6",
|
||||||
"@types/diff": "4.0.2",
|
"@types/diff": "4.0.2",
|
||||||
"@types/domhandler": "2.4.1",
|
"@types/domhandler": "2.4.1",
|
||||||
"@types/emoji-mart": "3.0.2",
|
|
||||||
"@types/highlight.js": "9.12.4",
|
"@types/highlight.js": "9.12.4",
|
||||||
"@types/jest": "26.0.14",
|
"@types/jest": "26.0.14",
|
||||||
"@types/js-yaml": "3.12.5",
|
"@types/js-yaml": "3.12.5",
|
||||||
|
@ -41,7 +40,8 @@
|
||||||
"copy-webpack-plugin": "6.2.1",
|
"copy-webpack-plugin": "6.2.1",
|
||||||
"d3-graphviz": "3.1.0",
|
"d3-graphviz": "3.1.0",
|
||||||
"diff": "4.0.2",
|
"diff": "4.0.2",
|
||||||
"emoji-mart": "3.0.0",
|
"emoji-picker-element": "^1.2.1",
|
||||||
|
"emojibase-data": "5",
|
||||||
"eslint-config-react-app": "5.2.1",
|
"eslint-config-react-app": "5.2.1",
|
||||||
"eslint-config-standard": "14.1.1",
|
"eslint-config-standard": "14.1.1",
|
||||||
"eslint-plugin-flowtype": "5.2.0",
|
"eslint-plugin-flowtype": "5.2.0",
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||||
import { Data, EmojiData, NimbleEmojiIndex } from 'emoji-mart'
|
import Database from 'emoji-picker-element/database'
|
||||||
import data from 'emoji-mart/data/twitter.json'
|
import { Emoji, EmojiClickEventDetail, NativeEmoji } from 'emoji-picker-element/shared'
|
||||||
import { customEmojis } from '../tool-bar/emoji-picker/emoji-picker'
|
import { customEmojis } from '../tool-bar/emoji-picker/emoji-picker'
|
||||||
import { getEmojiIcon, getEmojiShortCode } from '../tool-bar/utils/emojiUtils'
|
import { getEmojiIcon, getEmojiShortCode } from '../tool-bar/utils/emojiUtils'
|
||||||
import { findWordAtCursor, Hinter } from './index'
|
import { findWordAtCursor, Hinter } from './index'
|
||||||
|
|
||||||
const emojiIndex = new NimbleEmojiIndex(data as unknown as Data)
|
const emojiIndex = new Database({
|
||||||
|
customEmoji: customEmojis,
|
||||||
|
dataSource: '/static/js/emoji-data.json'
|
||||||
|
})
|
||||||
const emojiWordRegex = /^:([\w-_+]*)$/
|
const emojiWordRegex = /^:([\w-_+]*)$/
|
||||||
|
|
||||||
const generateEmojiHints = (editor: Editor): Promise< Hints| null > => {
|
const generateEmojiHints = (editor: Editor): Promise< Hints| null > => {
|
||||||
|
@ -17,23 +20,25 @@ const generateEmojiHints = (editor: Editor): Promise< Hints| null > => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const term = searchResult[1]
|
const term = searchResult[1]
|
||||||
let search: EmojiData[] | null = emojiIndex.search(term, {
|
let suggestionList: Emoji[]
|
||||||
emojisToShowFilter: () => true,
|
emojiIndex.getEmojiBySearchQuery(term)
|
||||||
maxResults: 7,
|
.then(async (result) => {
|
||||||
include: [],
|
suggestionList = result
|
||||||
exclude: [],
|
if (result.length === 0) {
|
||||||
custom: customEmojis as EmojiData[]
|
suggestionList = await emojiIndex.getTopFavoriteEmoji(7)
|
||||||
})
|
|
||||||
if (search === null) {
|
|
||||||
// set search to the first seven emojis in data
|
|
||||||
search = Object.values(emojiIndex.emojis).slice(0, 7)
|
|
||||||
}
|
}
|
||||||
const cursor = editor.getCursor()
|
const cursor = editor.getCursor()
|
||||||
if (!search) {
|
const skinTone = await emojiIndex.getPreferredSkinTone()
|
||||||
resolve(null)
|
const emojiEventDetails: EmojiClickEventDetail[] = suggestionList.map((emoji) => {
|
||||||
} else {
|
return {
|
||||||
|
emoji,
|
||||||
|
skinTone: skinTone,
|
||||||
|
unicode: ((emoji as NativeEmoji).unicode ? (emoji as NativeEmoji).unicode : undefined),
|
||||||
|
name: emoji.name
|
||||||
|
}
|
||||||
|
})
|
||||||
resolve({
|
resolve({
|
||||||
list: search.map((emojiData): Hint => ({
|
list: emojiEventDetails.map((emojiData): Hint => ({
|
||||||
text: getEmojiShortCode(emojiData),
|
text: getEmojiShortCode(emojiData),
|
||||||
render: (parent: HTMLLIElement) => {
|
render: (parent: HTMLLIElement) => {
|
||||||
const wrapper = document.createElement('div')
|
const wrapper = document.createElement('div')
|
||||||
|
@ -44,7 +49,11 @@ const generateEmojiHints = (editor: Editor): Promise< Hints| null > => {
|
||||||
from: Pos(cursor.line, searchTerm.start),
|
from: Pos(cursor.line, searchTerm.start),
|
||||||
to: Pos(cursor.line, searchTerm.end)
|
to: Pos(cursor.line, searchTerm.end)
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
resolve(null)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,13 @@ export const EmojiPickerButton: React.FC<EmojiPickerButtonProps> = ({ editor })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<EmojiPicker show={showEmojiPicker} onEmojiSelected={(emoji) => {
|
<EmojiPicker
|
||||||
|
show={showEmojiPicker}
|
||||||
|
onEmojiSelected={(emoji) => {
|
||||||
setShowEmojiPicker(false)
|
setShowEmojiPicker(false)
|
||||||
addEmoji(emoji, editor)
|
addEmoji(emoji, editor)
|
||||||
}} onDismiss={() => setShowEmojiPicker(false)}/>
|
}}
|
||||||
|
onDismiss={() => setShowEmojiPicker(false)}/>
|
||||||
<Button variant='light' onClick={() => setShowEmojiPicker(old => !old)} title={t('editor.editorToolbar.emoji')}>
|
<Button variant='light' onClick={() => setShowEmojiPicker(old => !old)} title={t('editor.editorToolbar.emoji')}>
|
||||||
<ForkAwesomeIcon icon="smile-o"/>
|
<ForkAwesomeIcon icon="smile-o"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
@import '../../../../../../node_modules/emoji-mart/css/emoji-mart';
|
.emoji-picker-container {
|
||||||
|
z-index: 1111;
|
||||||
.emoji-mart {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-emoji-native {
|
|
||||||
font-family: "twemoji", monospace;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,81 @@
|
||||||
import { CustomEmoji, Data, EmojiData, NimblePicker } from 'emoji-mart'
|
import { Picker } from 'emoji-picker-element'
|
||||||
import emojiData from 'emoji-mart/data/twitter.json'
|
import { CustomEmoji, EmojiClickEvent, EmojiClickEventDetail } from 'emoji-picker-element/shared'
|
||||||
import React, { useRef } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import { useClickAway } from 'react-use'
|
import { useClickAway } from 'react-use'
|
||||||
import { ShowIf } from '../../../../common/show-if/show-if'
|
import { ApplicationState } from '../../../../../redux'
|
||||||
import './emoji-picker.scss'
|
import './emoji-picker.scss'
|
||||||
import forkawesomeIcon from './forkawesome.png'
|
import forkawesomeIcon from './forkawesome.png'
|
||||||
import { ForkAwesomeIcons } from './icon-names'
|
import { ForkAwesomeIcons } from './icon-names'
|
||||||
|
|
||||||
export interface EmojiPickerProps {
|
export interface EmojiPickerProps {
|
||||||
show: boolean
|
show: boolean
|
||||||
onEmojiSelected: (emoji: EmojiData) => void
|
onEmojiSelected: (emoji: EmojiClickEventDetail) => void
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const customEmojis: CustomEmoji[] = Object.keys(ForkAwesomeIcons).map((name) => ({
|
export const customEmojis: CustomEmoji[] = Object.keys(ForkAwesomeIcons).map((name) => ({
|
||||||
name: `fa-${name}`,
|
name: `fa-${name}`,
|
||||||
short_names: [`fa-${name.toLowerCase()}`],
|
shortcodes: [`fa-${name.toLowerCase()}`],
|
||||||
text: '',
|
url: forkawesomeIcon,
|
||||||
emoticons: [],
|
category: 'ForkAwesome'
|
||||||
keywords: ['fork awesome'],
|
|
||||||
imageUrl: forkawesomeIcon,
|
|
||||||
customCategory: 'ForkAwesome'
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const EmojiPicker: React.FC<EmojiPickerProps> = ({ show, onEmojiSelected, onDismiss }) => {
|
export const EmojiPicker: React.FC<EmojiPickerProps> = ({ show, onEmojiSelected, onDismiss }) => {
|
||||||
const pickerRef = useRef(null)
|
const darkModeEnabled = useSelector((state: ApplicationState) => state.darkMode.darkMode)
|
||||||
|
const pickerContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const firstOpened = useRef(false)
|
||||||
|
|
||||||
useClickAway(pickerRef, () => {
|
useClickAway(pickerContainerRef, () => {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emojiClickListener = useCallback((event) => {
|
||||||
|
onEmojiSelected((event as EmojiClickEvent).detail)
|
||||||
|
}, [onEmojiSelected])
|
||||||
|
|
||||||
|
const twemojiStyle = useMemo(() => {
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.textContent = 'section.picker { --font-family: "twemoji" !important; }'
|
||||||
|
return style
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pickerContainerRef.current || firstOpened.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const picker = new Picker({
|
||||||
|
customEmoji: customEmojis,
|
||||||
|
dataSource: '/static/js/emoji-data.json'
|
||||||
|
})
|
||||||
|
const container = pickerContainerRef.current
|
||||||
|
picker.addEventListener('emoji-click', emojiClickListener)
|
||||||
|
if (picker.shadowRoot) {
|
||||||
|
picker.shadowRoot.appendChild(twemojiStyle)
|
||||||
|
}
|
||||||
|
container.appendChild(picker)
|
||||||
|
firstOpened.current = true
|
||||||
|
}, [pickerContainerRef, emojiClickListener, darkModeEnabled, twemojiStyle])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pickerContainerRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const pickerDomList = pickerContainerRef.current.getElementsByTagName('emoji-picker')
|
||||||
|
if (pickerDomList.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const picker = pickerDomList[0]
|
||||||
|
picker.setAttribute('class', darkModeEnabled ? 'dark' : 'light')
|
||||||
|
if (darkModeEnabled) {
|
||||||
|
picker.removeAttribute('style')
|
||||||
|
} else {
|
||||||
|
picker.setAttribute('style', '--background: #f8f9fa')
|
||||||
|
}
|
||||||
|
}, [darkModeEnabled, pickerContainerRef, firstOpened])
|
||||||
|
|
||||||
|
// noinspection CheckTagEmptyBody
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={show}>
|
<div className={`position-absolute emoji-picker-container ${!show ? 'd-none' : ''}`} ref={pickerContainerRef}></div>
|
||||||
<div className={'position-relative'} ref={pickerRef}>
|
|
||||||
<NimblePicker
|
|
||||||
data={emojiData as unknown as Data}
|
|
||||||
native={true}
|
|
||||||
onSelect={onEmojiSelected}
|
|
||||||
theme={'auto'}
|
|
||||||
title=''
|
|
||||||
custom={customEmojis}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ShowIf>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
import { BaseEmoji, CustomEmoji, EmojiData } from 'emoji-mart'
|
import { EmojiClickEventDetail, NativeEmoji } from 'emoji-picker-element/shared'
|
||||||
|
|
||||||
export const getEmojiIcon = (emoji: EmojiData):string => {
|
export const getEmojiIcon = (emoji: EmojiClickEventDetail): string => {
|
||||||
if ((emoji as BaseEmoji).native) {
|
if (emoji.unicode) {
|
||||||
return (emoji as BaseEmoji).native
|
return emoji.unicode
|
||||||
} else if ((emoji as CustomEmoji).imageUrl) {
|
}
|
||||||
|
if (emoji.name) {
|
||||||
// noinspection CheckTagEmptyBody
|
// noinspection CheckTagEmptyBody
|
||||||
return `<i class="fa ${(emoji as CustomEmoji).name}"></i>`
|
return `<i class="fa ${emoji.name}"></i>`
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEmojiShortCode = (emoji: EmojiData):string => {
|
export const getEmojiShortCode = (emoji: EmojiClickEventDetail): string => {
|
||||||
return (emoji as BaseEmoji).colons
|
let skinToneModifier = ''
|
||||||
|
if ((emoji.emoji as NativeEmoji).skins && emoji.skinTone !== 0) {
|
||||||
|
skinToneModifier = `:skin-tone-${emoji.skinTone as number}:`
|
||||||
|
}
|
||||||
|
return `:${emoji.emoji.shortcodes[0]}:${skinToneModifier}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Editor, Position, Range } from 'codemirror'
|
import CodeMirror, { Editor, Position, Range } from 'codemirror'
|
||||||
import { EmojiData } from 'emoji-mart'
|
import { EmojiClickEventDetail } from 'emoji-picker-element/shared'
|
||||||
import { Mock } from 'ts-mockery'
|
import { Mock } from 'ts-mockery'
|
||||||
import {
|
import {
|
||||||
addCodeFences,
|
addCodeFences,
|
||||||
|
@ -1762,8 +1762,23 @@ describe('test addTable', () => {
|
||||||
describe('test addEmoji with native emoji', () => {
|
describe('test addEmoji with native emoji', () => {
|
||||||
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
||||||
const textFirstLine = testContent.split('\n')[0]
|
const textFirstLine = testContent.split('\n')[0]
|
||||||
const emoji = Mock.of<EmojiData>({
|
const emoji = Mock.of<EmojiClickEventDetail>({
|
||||||
colons: ':+1:'
|
emoji: {
|
||||||
|
annotation: 'input numbers',
|
||||||
|
group: 8,
|
||||||
|
order: 3809,
|
||||||
|
shortcodes: [
|
||||||
|
'1234'
|
||||||
|
],
|
||||||
|
tags: [
|
||||||
|
'1234',
|
||||||
|
'input',
|
||||||
|
'numbers'
|
||||||
|
],
|
||||||
|
unicode: '🔢',
|
||||||
|
version: 0.6
|
||||||
|
},
|
||||||
|
unicode: '🔢'
|
||||||
})
|
})
|
||||||
it('just cursor', done => {
|
it('just cursor', done => {
|
||||||
Mock.extend(editor).with({
|
Mock.extend(editor).with({
|
||||||
|
@ -1778,7 +1793,7 @@ describe('test addEmoji with native emoji', () => {
|
||||||
),
|
),
|
||||||
getLine: (): string => (textFirstLine),
|
getLine: (): string => (textFirstLine),
|
||||||
replaceRange: (replacement: string | string[]) => {
|
replaceRange: (replacement: string | string[]) => {
|
||||||
expect(replacement).toEqual(':+1:')
|
expect(replacement).toEqual(':1234:')
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1800,7 +1815,7 @@ describe('test addEmoji with native emoji', () => {
|
||||||
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
|
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
|
||||||
expect(from).toEqual(firstLine.from)
|
expect(from).toEqual(firstLine.from)
|
||||||
expect(to).toEqual(firstLine.to)
|
expect(to).toEqual(firstLine.to)
|
||||||
expect(replacement).toEqual(':+1:')
|
expect(replacement).toEqual(':1234:')
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1822,7 +1837,7 @@ describe('test addEmoji with native emoji', () => {
|
||||||
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
|
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
|
||||||
expect(from).toEqual(multiline.from)
|
expect(from).toEqual(multiline.from)
|
||||||
expect(to).toEqual(multiline.to)
|
expect(to).toEqual(multiline.to)
|
||||||
expect(replacement).toEqual(':+1:')
|
expect(replacement).toEqual(':1234:')
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1844,7 +1859,7 @@ describe('test addEmoji with native emoji', () => {
|
||||||
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
|
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
|
||||||
expect(from).toEqual(multilineOffset.from)
|
expect(from).toEqual(multilineOffset.from)
|
||||||
expect(to).toEqual(multilineOffset.to)
|
expect(to).toEqual(multilineOffset.to)
|
||||||
expect(replacement).toEqual(':+1:')
|
expect(replacement).toEqual(':1234:')
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1856,10 +1871,16 @@ describe('test addEmoji with native emoji', () => {
|
||||||
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
||||||
const textFirstLine = testContent.split('\n')[0]
|
const textFirstLine = testContent.split('\n')[0]
|
||||||
const forkAwesomeIcon = ':fa-star:'
|
const forkAwesomeIcon = ':fa-star:'
|
||||||
const emoji = Mock.of<EmojiData>({
|
const emoji = Mock.of<EmojiClickEventDetail>({
|
||||||
name: 'star',
|
emoji: {
|
||||||
colons: ':fa-star:',
|
name: 'fa-star',
|
||||||
imageUrl: '/img/forkawesome.png'
|
shortcodes: [
|
||||||
|
'fa-star'
|
||||||
|
],
|
||||||
|
url: '/img/forkawesome.png'
|
||||||
|
},
|
||||||
|
skinTone: 0,
|
||||||
|
name: 'fa-star'
|
||||||
})
|
})
|
||||||
it('just cursor', done => {
|
it('just cursor', done => {
|
||||||
Mock.extend(editor).with({
|
Mock.extend(editor).with({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Editor } from 'codemirror'
|
import { Editor } from 'codemirror'
|
||||||
import { EmojiData } from 'emoji-mart'
|
import { EmojiClickEventDetail } from 'emoji-picker-element/shared'
|
||||||
import { getEmojiShortCode } from './emojiUtils'
|
import { getEmojiShortCode } from './emojiUtils'
|
||||||
|
|
||||||
export const makeSelectionBold = (editor: Editor): void => wrapTextWith(editor, '**')
|
export const makeSelectionBold = (editor: Editor): void => wrapTextWith(editor, '**')
|
||||||
|
@ -25,7 +25,7 @@ export const addCollapsableBlock = (editor: Editor): void => changeLines(editor,
|
||||||
export const addComment = (editor: Editor): void => changeLines(editor, line => `${line}\n> []`)
|
export const addComment = (editor: Editor): void => changeLines(editor, line => `${line}\n> []`)
|
||||||
export const addTable = (editor: Editor): void => changeLines(editor, line => `${line}\n| # 1 | # 2 | # 3 |\n| ---- | ---- | ---- |\n| Text | Text | Text |`)
|
export const addTable = (editor: Editor): void => changeLines(editor, line => `${line}\n| # 1 | # 2 | # 3 |\n| ---- | ---- | ---- |\n| Text | Text | Text |`)
|
||||||
|
|
||||||
export const addEmoji = (emoji: EmojiData, editor: Editor): void => {
|
export const addEmoji = (emoji: EmojiClickEventDetail, editor: Editor): void => {
|
||||||
insertAtCursor(editor, getEmojiShortCode(emoji))
|
insertAtCursor(editor, getEmojiShortCode(emoji))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,40 @@
|
||||||
import emojiData from 'emoji-mart/data/twitter.json'
|
|
||||||
import { Data } from 'emoji-mart/dist-es/utils/data'
|
|
||||||
import { ForkAwesomeIcons } from '../../../editor/editor-pane/tool-bar/emoji-picker/icon-names'
|
import { ForkAwesomeIcons } from '../../../editor/editor-pane/tool-bar/emoji-picker/icon-names'
|
||||||
|
import emojiData from 'emojibase-data/en/compact.json'
|
||||||
|
|
||||||
export const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
|
interface EmojiEntry {
|
||||||
.reduce((reduceObject, emojiIdentifier) => {
|
shortcodes: string[]
|
||||||
const emoji = (emojiData as unknown as Data).emojis[emojiIdentifier]
|
unicode: string
|
||||||
const emojiCodes = emoji.unified ?? emoji.b
|
}
|
||||||
if (emojiCodes) {
|
|
||||||
reduceObject[emojiIdentifier] = emojiCodes.split('-').map(char => `&#x${char};`).join('')
|
type ShortCodeMap = { [key: string]: string }
|
||||||
}
|
|
||||||
|
const shortCodeMap = (emojiData as unknown as EmojiEntry[])
|
||||||
|
.reduce((reduceObject, emoji) => {
|
||||||
|
emoji.shortcodes.forEach(shortcode => {
|
||||||
|
reduceObject[shortcode] = emoji.unicode
|
||||||
|
})
|
||||||
return reduceObject
|
return reduceObject
|
||||||
}, {} as { [key: string]: string })
|
}, {} as ShortCodeMap)
|
||||||
|
|
||||||
export const emojiSkinToneModifierMap = [2, 3, 4, 5, 6]
|
const emojiSkinToneModifierMap = [1, 2, 3, 4, 5]
|
||||||
.reduce((reduceObject, modifierValue) => {
|
.reduce((reduceObject, modifierValue) => {
|
||||||
const lightSkinCode = 127995
|
const lightSkinCode = 127995
|
||||||
const codepoint = lightSkinCode + (modifierValue - 2)
|
const codepoint = lightSkinCode + (modifierValue - 1)
|
||||||
const shortcode = `skin-tone-${modifierValue}`
|
const shortcode = `skin-tone-${modifierValue}`
|
||||||
reduceObject[shortcode] = `&#${codepoint};`
|
reduceObject[shortcode] = `&#${codepoint};`
|
||||||
return reduceObject
|
return reduceObject
|
||||||
}, {} as { [key: string]: string })
|
}, {} as ShortCodeMap)
|
||||||
|
|
||||||
export const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
|
const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
|
||||||
.reduce((reduceObject, icon) => {
|
.reduce((reduceObject, icon) => {
|
||||||
const shortcode = `fa-${icon}`
|
const shortcode = `fa-${icon}`
|
||||||
// noinspection CheckTagEmptyBody
|
// noinspection CheckTagEmptyBody
|
||||||
reduceObject[shortcode] = `<i class="fa fa-${icon}"></i>`
|
reduceObject[shortcode] = `<i class="fa fa-${icon}"></i>`
|
||||||
return reduceObject
|
return reduceObject
|
||||||
}, {} as { [key: string]: string })
|
}, {} as ShortCodeMap)
|
||||||
|
|
||||||
export const combinedEmojiData = {
|
export const combinedEmojiData = {
|
||||||
...markdownItTwitterEmojis,
|
...shortCodeMap,
|
||||||
...emojiSkinToneModifierMap,
|
...emojiSkinToneModifierMap,
|
||||||
...forkAwesomeIconMap
|
...forkAwesomeIconMap
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
25
yarn.lock
25
yarn.lock
|
@ -2066,13 +2066,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
domhandler "^2.4.0"
|
domhandler "^2.4.0"
|
||||||
|
|
||||||
"@types/emoji-mart@3.0.2":
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-3.0.2.tgz#5814064ce7c622069adf1583e17b3851a00802cb"
|
|
||||||
integrity sha512-Cmq8xpPK5Va+fjQE7ZaE5oykXzACBQ64CpNnYOIU7gWcR6nYTxWjMR3yPhnAMzw4yQn9R9761FpTvAyi/SH9MQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/react" "*"
|
|
||||||
|
|
||||||
"@types/eslint-visitor-keys@^1.0.0":
|
"@types/eslint-visitor-keys@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
||||||
|
@ -5755,13 +5748,10 @@ elliptic@^6.5.3:
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.0"
|
minimalistic-crypto-utils "^1.0.0"
|
||||||
|
|
||||||
emoji-mart@3.0.0:
|
emoji-picker-element@^1.2.1:
|
||||||
version "3.0.0"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-3.0.0.tgz#eca24a04881e27752a6921e09f65a86ce8539a50"
|
resolved "https://registry.yarnpkg.com/emoji-picker-element/-/emoji-picker-element-1.2.1.tgz#a8eb99035e07f970c16e202a6e0588dce15dda02"
|
||||||
integrity sha512-r5DXyzOLJttdwRYfJmPq/XL3W5tiAE/VsRnS0Hqyn27SqPA/GOYwVUSx50px/dXdJyDSnvmoPbuJ/zzhwSaU4A==
|
integrity sha512-gk0NBg7G/S6ClfIUjRKchXLrl4o1dcvKmEamFT9GERHfCeAyi+afUeMhwVY168I65RiqjGCJkGpoTV2CVa2QNA==
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.0.0"
|
|
||||||
prop-types "^15.6.0"
|
|
||||||
|
|
||||||
emoji-regex@^7.0.1, emoji-regex@^7.0.2:
|
emoji-regex@^7.0.1, emoji-regex@^7.0.2:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
|
@ -5778,6 +5768,11 @@ emoji-regex@^9.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.0.0.tgz#48a2309cc8a1d2e9d23bc6a67c39b63032e76ea4"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.0.0.tgz#48a2309cc8a1d2e9d23bc6a67c39b63032e76ea4"
|
||||||
integrity sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w==
|
integrity sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w==
|
||||||
|
|
||||||
|
emojibase-data@5:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-5.1.1.tgz#0a0d63dd07ce1376b3d27642f28cafa46f651de6"
|
||||||
|
integrity sha512-za/ma5SfogHjwUmGFnDbTvSfm8GGFvFaPS27GPti16YZSp5EPgz+UDsZCATXvJGit+oRNBbG/FtybXHKi2UQgQ==
|
||||||
|
|
||||||
emojis-list@^2.0.0:
|
emojis-list@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||||
|
@ -11513,7 +11508,7 @@ prop-types-extra@^1.1.0:
|
||||||
react-is "^16.3.2"
|
react-is "^16.3.2"
|
||||||
warning "^4.0.0"
|
warning "^4.0.0"
|
||||||
|
|
||||||
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||||
version "15.7.2"
|
version "15.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue