Add emoji picker (#329)

* Added emoji-mart as emoji-picker
* Fixed JSON to TypeScript-object parsing
* added fork awesome to emoji-picker
added ForkAwesomeIcons enum, because it's not possible to iterate over a typescript type consisting of strings [1]. This is a bit unfortunate since we now have two lists of all the fork awesome icons, but sadly it can not be done another way.
added fork awesome as a custom category to the emoji picker.

[1]: https://stackoverflow.com/questions/40863488/how-to-iterate-over-a-custom-literal-type-in-typescript
* made picker close, when clicking away
added react-use dependency for useClickAway hook
* Fixed emoji-picker loading images from unpkg instead of using font
* fixed addEmoji function
added tests
* Extract customIcons into useMemo

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: Erik Michelson <github@erik.michelson.eu>
Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Philip Molares 2020-08-07 18:54:37 +02:00 committed by GitHub
parent fc2e2bd592
commit c410a58573
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1321 additions and 76 deletions

View file

@ -0,0 +1,8 @@
.emoji-mart {
position: absolute;
z-index: 10000;
}
.emoji-mart-emoji-native {
font-family: "twemoji", monospace;
}

View file

@ -0,0 +1,47 @@
import { 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 { useClickAway } from 'react-use'
import { ShowIf } from '../../../../common/show-if/show-if'
import './emoji-picker.scss'
import { ForkAwesomeIcons } from './icon-names'
export interface EmojiPickerProps {
show: boolean
onEmojiSelected: (emoji: EmojiData) => void
onDismiss: () => void
}
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()
})
return (
<ShowIf condition={show}>
<div className={'position-relative'} ref={pickerRef}>
<NimblePicker
data={emojiData as unknown as Data}
native={true}
onSelect={onEmojiSelected}
theme={'auto'}
title=''
custom={customIcons}
/>
</div>
</ShowIf>
)
}

View file

@ -0,0 +1,760 @@
export enum ForkAwesomeIcons {
'500px'='500px',
'activitypub'='activitypub',
'address-book-o'='address-book-o',
'address-book'='address-book',
'address-card-o'='address-card-o',
'address-card'='address-card',
'adjust'='adjust',
'adn'='adn',
'align-center'='align-center',
'align-justify'='align-justify',
'align-left'='align-left',
'align-right'='align-right',
'amazon'='amazon',
'ambulance'='ambulance',
'american-sign-language-interpreting'='american-sign-language-interpreting',
'anchor'='anchor',
'android'='android',
'angellist'='angellist',
'angle-double-down'='angle-double-down',
'angle-double-left'='angle-double-left',
'angle-double-right'='angle-double-right',
'angle-double-up'='angle-double-up',
'angle-down'='angle-down',
'angle-left'='angle-left',
'angle-right'='angle-right',
'angle-up'='angle-up',
'apple'='apple',
'archive-org'='archive-org',
'archive'='archive',
'archlinux'='archlinux',
'area-chart'='area-chart',
'arrow-circle-down'='arrow-circle-down',
'arrow-circle-left'='arrow-circle-left',
'arrow-circle-o-down'='arrow-circle-o-down',
'arrow-circle-o-left'='arrow-circle-o-left',
'arrow-circle-o-right'='arrow-circle-o-right',
'arrow-circle-o-up'='arrow-circle-o-up',
'arrow-circle-right'='arrow-circle-right',
'arrow-circle-up'='arrow-circle-up',
'arrow-down'='arrow-down',
'arrow-left'='arrow-left',
'arrow-right'='arrow-right',
'arrows-alt'='arrows-alt',
'arrows-h'='arrows-h',
'arrows'='arrows',
'arrows-v'='arrows-v',
'arrow-up'='arrow-up',
'artstation'='artstation',
'assistive-listening-systems'='assistive-listening-systems',
'asterisk'='asterisk',
'at'='at',
'att'='att',
'audio-description'='audio-description',
'backward'='backward',
'balance-scale'='balance-scale',
'bandcamp'='bandcamp',
'ban'='ban',
'bar-chart'='bar-chart',
'barcode'='barcode',
'bars'='bars',
'bath'='bath',
'battery-empty'='battery-empty',
'battery-full'='battery-full',
'battery-half'='battery-half',
'battery-quarter'='battery-quarter',
'battery-three-quarters'='battery-three-quarters',
'bed'='bed',
'beer'='beer',
'behance-square'='behance-square',
'behance'='behance',
'bell-o'='bell-o',
'bell-rigning-o'='bell-rigning-o',
'bell-ringing'='bell-ringing',
'bell-slash-o'='bell-slash-o',
'bell-slash'='bell-slash',
'bell'='bell',
'bicycle'='bicycle',
'binoculars'='binoculars',
'biometric'='biometric',
'birthday-cake'='birthday-cake',
'bitbucket-square'='bitbucket-square',
'bitbucket'='bitbucket',
'black-tie'='black-tie',
'blind'='blind',
'bluetooth-b'='bluetooth-b',
'bluetooth'='bluetooth',
'bold'='bold',
'bolt'='bolt',
'bomb'='bomb',
'bookmark-o'='bookmark-o',
'bookmark'='bookmark',
'book'='book',
'bootstrap'='bootstrap',
'braille'='braille',
'briefcase'='briefcase',
'btc'='btc',
'bug'='bug',
'building-o'='building-o',
'building'='building',
'bullhorn'='bullhorn',
'bullseye'='bullseye',
'bus'='bus',
'buysellads'='buysellads',
'calculator'='calculator',
'calendar-check-o'='calendar-check-o',
'calendar-minus-o'='calendar-minus-o',
'calendar-o'='calendar-o',
'calendar-plus-o'='calendar-plus-o',
'calendar'='calendar',
'calendar-times-o'='calendar-times-o',
'camera-retro'='camera-retro',
'camera'='camera',
'caret-down'='caret-down',
'caret-left'='caret-left',
'caret-right'='caret-right',
'caret-square-o-down'='caret-square-o-down',
'caret-square-o-left'='caret-square-o-left',
'caret-square-o-right'='caret-square-o-right',
'caret-square-o-up'='caret-square-o-up',
'caret-up'='caret-up',
'car'='car',
'cart-arrow-down'='cart-arrow-down',
'cart-plus'='cart-plus',
'cc-amex'='cc-amex',
'cc-diners-club'='cc-diners-club',
'cc-discover'='cc-discover',
'cc-jcb'='cc-jcb',
'cc-mastercard'='cc-mastercard',
'cc-paypal'='cc-paypal',
'cc-stripe'='cc-stripe',
'cc'='cc',
'cc-visa'='cc-visa',
'certificate'='certificate',
'chain-broken'='chain-broken',
'check-circle-o'='check-circle-o',
'check-circle'='check-circle',
'check-square-o'='check-square-o',
'check-square'='check-square',
'check'='check',
'chevron-circle-down'='chevron-circle-down',
'chevron-circle-left'='chevron-circle-left',
'chevron-circle-right'='chevron-circle-right',
'chevron-circle-up'='chevron-circle-up',
'chevron-down'='chevron-down',
'chevron-left'='chevron-left',
'chevron-right'='chevron-right',
'chevron-up'='chevron-up',
'child'='child',
'chrome'='chrome',
'circle-o-notch'='circle-o-notch',
'circle-o'='circle-o',
'circle'='circle',
'circle-thin'='circle-thin',
'classicpress-circle'='classicpress-circle',
'classicpress'='classicpress',
'clipboard'='clipboard',
'clock-o'='clock-o',
'clone'='clone',
'cloud-download'='cloud-download',
'cloud'='cloud',
'cloud-upload'='cloud-upload',
'code-fork'='code-fork',
'codepen'='codepen',
'code'='code',
'codiepie'='codiepie',
'coffee'='coffee',
'cogs'='cogs',
'cog'='cog',
'columns'='columns',
'commenting-o'='commenting-o',
'commenting'='commenting',
'comment-o'='comment-o',
'comments-o'='comments-o',
'comments'='comments',
'comment'='comment',
'compass'='compass',
'compress'='compress',
'connectdevelop'='connectdevelop',
'contao'='contao',
'copyright'='copyright',
'creative-commons'='creative-commons',
'credit-card-alt'='credit-card-alt',
'credit-card'='credit-card',
'crop'='crop',
'crosshairs'='crosshairs',
'css3'='css3',
'c'='c',
'cubes'='cubes',
'cube'='cube',
'cutlery'='cutlery',
'dashcube'='dashcube',
'database'='database',
'deaf'='deaf',
'debian'='debian',
'delicious'='delicious',
'desktop'='desktop',
'deviantart'='deviantart',
'dev-to'='dev-to',
'diamond'='diamond',
'diaspora'='diaspora',
'digg'='digg',
'digitalocean'='digitalocean',
'discord-alt'='discord-alt',
'discord'='discord',
'dogmazic'='dogmazic',
'dot-circle-o'='dot-circle-o',
'download'='download',
'dribbble'='dribbble',
'dropbox'='dropbox',
'drupal'='drupal',
'edge'='edge',
'eercast'='eercast',
'eject'='eject',
'ellipsis-h'='ellipsis-h',
'ellipsis-v'='ellipsis-v',
'emby'='emby',
'empire'='empire',
'envelope-open-o'='envelope-open-o',
'envelope-open'='envelope-open',
'envelope-o'='envelope-o',
'envelope-square'='envelope-square',
'envelope'='envelope',
'envira'='envira',
'eraser'='eraser',
'ethereum'='ethereum',
'etsy'='etsy',
'eur'='eur',
'exchange'='exchange',
'exclamation-circle'='exclamation-circle',
'exclamation'='exclamation',
'exclamation-triangle'='exclamation-triangle',
'expand'='expand',
'expeditedssl'='expeditedssl',
'external-link-square'='external-link-square',
'external-link'='external-link',
'eyedropper'='eyedropper',
'eye-slash'='eye-slash',
'eye'='eye',
'facebook-messenger'='facebook-messenger',
'facebook-official'='facebook-official',
'facebook-square'='facebook-square',
'facebook'='facebook',
'fast-backward'='fast-backward',
'fast-forward'='fast-forward',
'fax'='fax',
'f-droid'='f-droid',
'female'='female',
'ffmpeg'='ffmpeg',
'fighter-jet'='fighter-jet',
'file-archive-o'='file-archive-o',
'file-audio-o'='file-audio-o',
'file-code-o'='file-code-o',
'file-epub'='file-epub',
'file-excel-o'='file-excel-o',
'file-image-o'='file-image-o',
'file-o'='file-o',
'file-pdf-o'='file-pdf-o',
'file-powerpoint-o'='file-powerpoint-o',
'files-o'='files-o',
'file'='file',
'file-text-o'='file-text-o',
'file-text'='file-text',
'file-video-o'='file-video-o',
'file-word-o'='file-word-o',
'film'='film',
'filter'='filter',
'fire-extinguisher'='fire-extinguisher',
'firefox'='firefox',
'fire'='fire',
'first-order'='first-order',
'flag-checkered'='flag-checkered',
'flag-o'='flag-o',
'flag'='flag',
'flask'='flask',
'flickr'='flickr',
'floppy-o'='floppy-o',
'folder-open-o'='folder-open-o',
'folder-open'='folder-open',
'folder-o'='folder-o',
'folder'='folder',
'font-awesome'='font-awesome',
'fonticons'='fonticons',
'font'='font',
'fork-awesome'='fork-awesome',
'fort-awesome'='fort-awesome',
'forumbee'='forumbee',
'forward'='forward',
'foursquare'='foursquare',
'free-code-camp'='free-code-camp',
'freedombox'='freedombox',
'friendica'='friendica',
'frown-o'='frown-o',
'funkwhale'='funkwhale',
'futbol-o'='futbol-o',
'gamepad'='gamepad',
'gavel'='gavel',
'gbp'='gbp',
'genderless'='genderless',
'get-pocket'='get-pocket',
'gg-circle'='gg-circle',
'gg'='gg',
'gift'='gift',
'gimp'='gimp',
'gitea'='gitea',
'github-alt'='github-alt',
'github-square'='github-square',
'github'='github',
'gitlab'='gitlab',
'git-square'='git-square',
'git'='git',
'glass'='glass',
'glide-g'='glide-g',
'glide'='glide',
'globe-e'='globe-e',
'globe'='globe',
'globe-w'='globe-w',
'gnupg'='gnupg',
'gnu-social'='gnu-social',
'google-plus-official'='google-plus-official',
'google-plus-square'='google-plus-square',
'google-plus'='google-plus',
'google'='google',
'google-wallet'='google-wallet',
'graduation-cap'='graduation-cap',
'gratipay'='gratipay',
'grav'='grav',
'hackaday'='hackaday',
'hacker-news'='hacker-news',
'hackster'='hackster',
'hal'='hal',
'hand-lizard-o'='hand-lizard-o',
'hand-o-down'='hand-o-down',
'hand-o-left'='hand-o-left',
'hand-o-right'='hand-o-right',
'hand-o-up'='hand-o-up',
'hand-paper-o'='hand-paper-o',
'hand-peace-o'='hand-peace-o',
'hand-pointer-o'='hand-pointer-o',
'hand-rock-o'='hand-rock-o',
'hand-scissors-o'='hand-scissors-o',
'handshake-o'='handshake-o',
'hand-spock-o'='hand-spock-o',
'hashnode'='hashnode',
'hashtag'='hashtag',
'hdd-o'='hdd-o',
'header'='header',
'headphones'='headphones',
'heartbeat'='heartbeat',
'heart-o'='heart-o',
'heart'='heart',
'history'='history',
'home'='home',
'hospital-o'='hospital-o',
'hourglass-end'='hourglass-end',
'hourglass-half'='hourglass-half',
'hourglass-o'='hourglass-o',
'hourglass-start'='hourglass-start',
'hourglass'='hourglass',
'houzz'='houzz',
'h-square'='h-square',
'html5'='html5',
'hubzilla'='hubzilla',
'i-cursor'='i-cursor',
'id-badge'='id-badge',
'id-card-o'='id-card-o',
'id-card'='id-card',
'ils'='ils',
'imdb'='imdb',
'inbox'='inbox',
'indent'='indent',
'industry'='industry',
'info-circle'='info-circle',
'info'='info',
'inkscape'='inkscape',
'inr'='inr',
'instagram'='instagram',
'internet-explorer'='internet-explorer',
'ioxhost'='ioxhost',
'italic'='italic',
'jirafeau'='jirafeau',
'joomla'='joomla',
'joplin'='joplin',
'jpy'='jpy',
'jsfiddle'='jsfiddle',
'julia'='julia',
'jupyter'='jupyter',
'keybase'='keybase',
'keyboard-o'='keyboard-o',
'key-modern'='key-modern',
'key'='key',
'krw'='krw',
'language'='language',
'laptop'='laptop',
'laravel'='laravel',
'lastfm-square'='lastfm-square',
'lastfm'='lastfm',
'leaf'='leaf',
'leanpub'='leanpub',
'lemon-o'='lemon-o',
'level-down'='level-down',
'level-up'='level-up',
'liberapay-square'='liberapay-square',
'liberapay'='liberapay',
'life-ring'='life-ring',
'lightbulb-o'='lightbulb-o',
'line-chart'='line-chart',
'linkedin-square'='linkedin-square',
'linkedin'='linkedin',
'link'='link',
'linode'='linode',
'linux'='linux',
'list-alt'='list-alt',
'list-ol'='list-ol',
'list'='list',
'list-ul'='list-ul',
'location-arrow'='location-arrow',
'lock'='lock',
'long-arrow-down'='long-arrow-down',
'long-arrow-left'='long-arrow-left',
'long-arrow-right'='long-arrow-right',
'long-arrow-up'='long-arrow-up',
'low-vision'='low-vision',
'magic'='magic',
'magnet'='magnet',
'male'='male',
'map-marker'='map-marker',
'map-o'='map-o',
'map-pin'='map-pin',
'map-signs'='map-signs',
'map'='map',
'mars-double'='mars-double',
'mars-stroke-h'='mars-stroke-h',
'mars-stroke'='mars-stroke',
'mars-stroke-v'='mars-stroke-v',
'mars'='mars',
'mastodon-alt'='mastodon-alt',
'mastodon-square'='mastodon-square',
'mastodon'='mastodon',
'matrix-org'='matrix-org',
'maxcdn'='maxcdn',
'meanpath'='meanpath',
'medium-square'='medium-square',
'medium'='medium',
'medkit'='medkit',
'meetup'='meetup',
'meh-o'='meh-o',
'mercury'='mercury',
'microchip'='microchip',
'microphone-slash'='microphone-slash',
'microphone'='microphone',
'minus-circle'='minus-circle',
'minus-square-o'='minus-square-o',
'minus-square'='minus-square',
'minus'='minus',
'mixcloud'='mixcloud',
'mobile'='mobile',
'modx'='modx',
'money'='money',
'moon-o'='moon-o',
'moon'='moon',
'motorcycle'='motorcycle',
'mouse-pointer'='mouse-pointer',
'music'='music',
'neuter'='neuter',
'newspaper-o'='newspaper-o',
'nextcloud-square'='nextcloud-square',
'nextcloud'='nextcloud',
'nodejs'='nodejs',
'object-group'='object-group',
'object-ungroup'='object-ungroup',
'odnoklassniki-square'='odnoklassniki-square',
'odnoklassniki'='odnoklassniki',
'opencart'='opencart',
'open-collective'='open-collective',
'openid'='openid',
'opera'='opera',
'optin-monster'='optin-monster',
'orcid'='orcid',
'outdent'='outdent',
'pagelines'='pagelines',
'paint-brush'='paint-brush',
'paperclip'='paperclip',
'paper-plane-o'='paper-plane-o',
'paper-plane'='paper-plane',
'paragraph'='paragraph',
'patreon'='patreon',
'pause-circle-o'='pause-circle-o',
'pause-circle'='pause-circle',
'pause'='pause',
'paw'='paw',
'paypal'='paypal',
'peertube'='peertube',
'pencil-square-o'='pencil-square-o',
'pencil-square'='pencil-square',
'pencil'='pencil',
'percent'='percent',
'phone-square'='phone-square',
'phone'='phone',
'php'='php',
'picture-o'='picture-o',
'pie-chart'='pie-chart',
'pinterest-p'='pinterest-p',
'pinterest-square'='pinterest-square',
'pinterest'='pinterest',
'pixelfed'='pixelfed',
'plane'='plane',
'play-circle-o'='play-circle-o',
'play-circle'='play-circle',
'play'='play',
'pleroma'='pleroma',
'plug'='plug',
'plus-circle'='plus-circle',
'plus-square-o'='plus-square-o',
'plus-square'='plus-square',
'plus'='plus',
'podcast'='podcast',
'power-off'='power-off',
'print'='print',
'product-hunt'='product-hunt',
'puzzle-piece'='puzzle-piece',
'python'='python',
'qq'='qq',
'qrcode'='qrcode',
'question-circle-o'='question-circle-o',
'question-circle'='question-circle',
'question'='question',
'quora'='quora',
'quote-left'='quote-left',
'quote-right'='quote-right',
'random'='random',
'ravelry'='ravelry',
'react'='react',
'rebel'='rebel',
'recycle'='recycle',
'reddit-alien'='reddit-alien',
'reddit-square'='reddit-square',
'reddit'='reddit',
'refresh'='refresh',
'registered'='registered',
'renren'='renren',
'repeat'='repeat',
'reply-all'='reply-all',
'reply'='reply',
'researchgate'='researchgate',
'retweet'='retweet',
'road'='road',
'rocket'='rocket',
'rss-square'='rss-square',
'rss'='rss',
'rub'='rub',
'safari'='safari',
'scissors'='scissors',
'scribd'='scribd',
'scuttlebutt'='scuttlebutt',
'search-minus'='search-minus',
'search-plus'='search-plus',
'search'='search',
'sellsy'='sellsy',
'server'='server',
'shaarli-o'='shaarli-o',
'shaarli'='shaarli',
'share-alt-square'='share-alt-square',
'share-alt'='share-alt',
'share-square-o'='share-square-o',
'share-square'='share-square',
'share'='share',
'shield'='shield',
'ship'='ship',
'shirtsinbulk'='shirtsinbulk',
'shopping-bag'='shopping-bag',
'shopping-basket'='shopping-basket',
'shopping-cart'='shopping-cart',
'shower'='shower',
'signalapp'='signalapp',
'signal'='signal',
'sign-in'='sign-in',
'sign-language'='sign-language',
'sign-out'='sign-out',
'simplybuilt'='simplybuilt',
'sitemap'='sitemap',
'skyatlas'='skyatlas',
'skype'='skype',
'slack'='slack',
'sliders'='sliders',
'slideshare'='slideshare',
'smile-o'='smile-o',
'snapchat-ghost'='snapchat-ghost',
'snapchat-square'='snapchat-square',
'snapchat'='snapchat',
'snowdrift'='snowdrift',
'snowflake-o'='snowflake-o',
'social-home'='social-home',
'sort-alpha-asc'='sort-alpha-asc',
'sort-alpha-desc'='sort-alpha-desc',
'sort-amount-asc'='sort-amount-asc',
'sort-amount-desc'='sort-amount-desc',
'sort-asc'='sort-asc',
'sort-desc'='sort-desc',
'sort-numeric-asc'='sort-numeric-asc',
'sort-numeric-desc'='sort-numeric-desc',
'sort'='sort',
'soundcloud'='soundcloud',
'space-shuttle'='space-shuttle',
'spell-check'='spell-check',
'spinner'='spinner',
'spoon'='spoon',
'spotify'='spotify',
'square-o'='square-o',
'square'='square',
'stack-exchange'='stack-exchange',
'stack-overflow'='stack-overflow',
'star-half-o'='star-half-o',
'star-half'='star-half',
'star-o'='star-o',
'star'='star',
'steam-square'='steam-square',
'steam'='steam',
'step-backward'='step-backward',
'step-forward'='step-forward',
'stethoscope'='stethoscope',
'sticky-note-o'='sticky-note-o',
'sticky-note'='sticky-note',
'stop-circle-o'='stop-circle-o',
'stop-circle'='stop-circle',
'stop'='stop',
'street-view'='street-view',
'strikethrough'='strikethrough',
'stumbleupon-circle'='stumbleupon-circle',
'stumbleupon'='stumbleupon',
'subscript'='subscript',
'subway'='subway',
'suitcase'='suitcase',
'sun-o'='sun-o',
'sun'='sun',
'superpowers'='superpowers',
'superscript'='superscript',
'syncthing'='syncthing',
'table'='table',
'tablet'='tablet',
'tachometer'='tachometer',
'tags'='tags',
'tag'='tag',
'tasks'='tasks',
'taxi'='taxi',
'telegram'='telegram',
'television'='television',
'tencent-weibo'='tencent-weibo',
'terminal'='terminal',
'text-height'='text-height',
'text-width'='text-width',
'themeisle'='themeisle',
'thermometer-empty'='thermometer-empty',
'thermometer-full'='thermometer-full',
'thermometer-half'='thermometer-half',
'thermometer-quarter'='thermometer-quarter',
'thermometer-three-quarters'='thermometer-three-quarters',
'th-large'='th-large',
'th-list'='th-list',
'th'='th',
'thumbs-down'='thumbs-down',
'thumbs-o-down'='thumbs-o-down',
'thumbs-o-up'='thumbs-o-up',
'thumbs-up'='thumbs-up',
'thumb-tack'='thumb-tack',
'ticket'='ticket',
'times-circle-o'='times-circle-o',
'times-circle'='times-circle',
'times'='times',
'tint'='tint',
'tipeee'='tipeee',
'toggle-off'='toggle-off',
'toggle-on'='toggle-on',
'tor-onion'='tor-onion',
'trademark'='trademark',
'train'='train',
'transgender-alt'='transgender-alt',
'transgender'='transgender',
'trash-o'='trash-o',
'trash'='trash',
'tree'='tree',
'trello'='trello',
'tripadvisor'='tripadvisor',
'trophy'='trophy',
'truck'='truck',
'try'='try',
'tty'='tty',
'tumblr-square'='tumblr-square',
'tumblr'='tumblr',
'twitch'='twitch',
'twitter-square'='twitter-square',
'twitter'='twitter',
'umbrella'='umbrella',
'underline'='underline',
'undo'='undo',
'universal-access'='universal-access',
'university'='university',
'unlock-alt'='unlock-alt',
'unlock'='unlock',
'unslpash'='unslpash',
'upload'='upload',
'usb'='usb',
'usd'='usd',
'user-circle-o'='user-circle-o',
'user-circle'='user-circle',
'user-md'='user-md',
'user-o'='user-o',
'user-plus'='user-plus',
'user-secret'='user-secret',
'users'='users',
'user'='user',
'user-times'='user-times',
'venus-double'='venus-double',
'venus-mars'='venus-mars',
'venus'='venus',
'viacoin'='viacoin',
'viadeo-square'='viadeo-square',
'viadeo'='viadeo',
'video-camera'='video-camera',
'vimeo-square'='vimeo-square',
'vimeo'='vimeo',
'vine'='vine',
'vk'='vk',
'volume-control-phone'='volume-control-phone',
'volume-down'='volume-down',
'volume-mute'='volume-mute',
'volume-off'='volume-off',
'volume-up'='volume-up',
'weibo'='weibo',
'weixin'='weixin',
'whatsapp'='whatsapp',
'wheelchair-alt'='wheelchair-alt',
'wheelchair'='wheelchair',
'wifi'='wifi',
'wikidata'='wikidata',
'wikipedia-w'='wikipedia-w',
'window-close-o'='window-close-o',
'window-close'='window-close',
'window-maximize'='window-maximize',
'window-minimize'='window-minimize',
'window-restore'='window-restore',
'windows'='windows',
'wire'='wire',
'wordpress'='wordpress',
'wpbeginner'='wpbeginner',
'wpexplorer'='wpexplorer',
'wpforms'='wpforms',
'wrench'='wrench',
'xing-square'='xing-square',
'xing'='xing',
'xmpp'='xmpp',
'yahoo'='yahoo',
'y-combinator'='y-combinator',
'yelp'='yelp',
'yoast'='yoast',
'youtube-play'='youtube-play',
'youtube-square'='youtube-square',
'youtube'='youtube',
'zotero'='zotero'
}

View file

@ -1,12 +1,14 @@
import { Editor } from 'codemirror'
import React from 'react'
import React, { Fragment, useState } from 'react'
import { Button, ButtonToolbar } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { EmojiPicker } from './emoji-picker/emoji-picker'
import './tool-bar.scss'
import {
addCodeFences,
addComment,
addEmoji,
addHeaderLevel,
addImage,
addLine,
@ -30,6 +32,8 @@ export interface ToolBarProps {
export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
const { t } = useTranslation()
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const notImplemented = () => {
alert('This feature is not yet implemented')
}
@ -39,61 +43,70 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
}
return (
<ButtonToolbar className='flex-nowrap bg-light'>
<Button variant='light' onClick={() => makeSelectionBold(editor)} title={t('editor.editorToolbar.bold')}>
<ForkAwesomeIcon icon="bold"/>
</Button>
<Button variant='light' onClick={() => makeSelectionItalic(editor)} title={t('editor.editorToolbar.italic')}>
<ForkAwesomeIcon icon="italic"/>
</Button>
<Button variant='light' onClick={() => underlineSelection(editor)} title={t('editor.editorToolbar.underline')}>
<ForkAwesomeIcon icon="underline"/>
</Button>
<Button variant='light' onClick={() => strikeThroughSelection(editor)} title={t('editor.editorToolbar.strikethrough')}>
<ForkAwesomeIcon icon="strikethrough"/>
</Button>
<Button variant='light' onClick={() => subscriptSelection(editor)} title={t('editor.editorToolbar.subscript')}>
<ForkAwesomeIcon icon="subscript"/>
</Button>
<Button variant='light' onClick={() => superscriptSelection(editor)} title={t('editor.editorToolbar.superscript')}>
<ForkAwesomeIcon icon="superscript"/>
</Button>
<Button variant='light' onClick={() => addHeaderLevel(editor)} title={t('editor.editorToolbar.header')}>
<ForkAwesomeIcon icon="header"/>
</Button>
<Button variant='light' onClick={() => addCodeFences(editor)} title={t('editor.editorToolbar.code')}>
<ForkAwesomeIcon icon="code"/>
</Button>
<Button variant='light' onClick={() => addQuotes(editor)} title={t('editor.editorToolbar.blockquote')}>
<ForkAwesomeIcon icon="quote-right"/>
</Button>
<Button variant='light' onClick={() => addList(editor)} title={t('editor.editorToolbar.unorderedList')}>
<ForkAwesomeIcon icon="list"/>
</Button>
<Button variant='light' onClick={() => addOrderedList(editor)} title={t('editor.editorToolbar.orderedList')}>
<ForkAwesomeIcon icon="list-ol"/>
</Button>
<Button variant='light' onClick={() => addTaskList(editor)} title={t('editor.editorToolbar.checkList')}>
<ForkAwesomeIcon icon="check-square"/>
</Button>
<Button variant='light' onClick={() => addLink(editor)} title={t('editor.editorToolbar.link')}>
<ForkAwesomeIcon icon="link"/>
</Button>
<Button variant='light' onClick={() => addImage(editor)} title={t('editor.editorToolbar.image')}>
<ForkAwesomeIcon icon="picture-o"/>
</Button>
<Button variant='light' onClick={notImplemented} title={t('editor.editorToolbar.uploadImage')}>
<ForkAwesomeIcon icon="upload"/>
</Button>
<Button variant='light' onClick={() => addTable(editor)} title={t('editor.editorToolbar.table')}>
<ForkAwesomeIcon icon="table"/>
</Button>
<Button variant='light' onClick={() => addLine(editor)} title={t('editor.editorToolbar.line')}>
<ForkAwesomeIcon icon="minus"/>
</Button>
<Button variant='light' onClick={() => addComment(editor)} title={t('editor.editorToolbar.comment')}>
<ForkAwesomeIcon icon="comment"/>
</Button>
</ButtonToolbar>
<Fragment>
<ButtonToolbar className='flex-nowrap bg-light'>
<Button variant='light' onClick={() => makeSelectionBold(editor)} title={t('editor.editorToolbar.bold')}>
<ForkAwesomeIcon icon="bold"/>
</Button>
<Button variant='light' onClick={() => makeSelectionItalic(editor)} title={t('editor.editorToolbar.italic')}>
<ForkAwesomeIcon icon="italic"/>
</Button>
<Button variant='light' onClick={() => underlineSelection(editor)} title={t('editor.editorToolbar.underline')}>
<ForkAwesomeIcon icon="underline"/>
</Button>
<Button variant='light' onClick={() => strikeThroughSelection(editor)} title={t('editor.editorToolbar.strikethrough')}>
<ForkAwesomeIcon icon="strikethrough"/>
</Button>
<Button variant='light' onClick={() => subscriptSelection(editor)} title={t('editor.editorToolbar.subscript')}>
<ForkAwesomeIcon icon="subscript"/>
</Button>
<Button variant='light' onClick={() => superscriptSelection(editor)} title={t('editor.editorToolbar.superscript')}>
<ForkAwesomeIcon icon="superscript"/>
</Button>
<Button variant='light' onClick={() => addHeaderLevel(editor)} title={t('editor.editorToolbar.header')}>
<ForkAwesomeIcon icon="header"/>
</Button>
<Button variant='light' onClick={() => addCodeFences(editor)} title={t('editor.editorToolbar.code')}>
<ForkAwesomeIcon icon="code"/>
</Button>
<Button variant='light' onClick={() => addQuotes(editor)} title={t('editor.editorToolbar.blockquote')}>
<ForkAwesomeIcon icon="quote-right"/>
</Button>
<Button variant='light' onClick={() => addList(editor)} title={t('editor.editorToolbar.unorderedList')}>
<ForkAwesomeIcon icon="list"/>
</Button>
<Button variant='light' onClick={() => addOrderedList(editor)} title={t('editor.editorToolbar.orderedList')}>
<ForkAwesomeIcon icon="list-ol"/>
</Button>
<Button variant='light' onClick={() => addTaskList(editor)} title={t('editor.editorToolbar.checkList')}>
<ForkAwesomeIcon icon="check-square"/>
</Button>
<Button variant='light' onClick={() => addLink(editor)} title={t('editor.editorToolbar.link')}>
<ForkAwesomeIcon icon="link"/>
</Button>
<Button variant='light' onClick={() => addImage(editor)} title={t('editor.editorToolbar.image')}>
<ForkAwesomeIcon icon="picture-o"/>
</Button>
<Button variant='light' onClick={notImplemented} title={t('editor.editorToolbar.uploadImage')}>
<ForkAwesomeIcon icon="upload"/>
</Button>
<Button variant='light' onClick={() => addTable(editor)} title={t('editor.editorToolbar.table')}>
<ForkAwesomeIcon icon="table"/>
</Button>
<Button variant='light' onClick={() => addLine(editor)} title={t('editor.editorToolbar.line')}>
<ForkAwesomeIcon icon="minus"/>
</Button>
<Button variant='light' onClick={() => addComment(editor)} title={t('editor.editorToolbar.comment')}>
<ForkAwesomeIcon icon="comment"/>
</Button>
<EmojiPicker show={showEmojiPicker} onEmojiSelected={(emoji) => {
setShowEmojiPicker(false)
addEmoji(emoji, editor)
}} onDismiss={() => setShowEmojiPicker(false)}/>
<Button variant='light' onClick={() => setShowEmojiPicker(old => !old)} title={''}>
<ForkAwesomeIcon icon="smile-o"/>
</Button>
</ButtonToolbar>
</Fragment>
)
}

View file

@ -1,8 +1,10 @@
import { Editor, Position, Range } from 'codemirror'
import { EmojiData } from 'emoji-mart'
import { Mock } from 'ts-mockery'
import {
addCodeFences,
addComment,
addEmoji,
addHeaderLevel,
addImage,
addLine,
@ -1639,3 +1641,192 @@ describe('test addTable', () => {
addTable(editor)
})
})
describe('test addEmoji with native emoji', () => {
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
const textFirstLine = testContent.split('\n')[0]
const emoji = Mock.of<EmojiData>({
native: '👍'
})
it('just cursor', done => {
Mock.extend(editor).with({
listSelections: () => (
Mock.of<Range[]>([{
anchor: cursor.from,
head: cursor.to,
from: () => cursor.from,
to: () => cursor.to,
empty: () => true
}])
),
getLine: (): string => (textFirstLine),
replaceRange: (replacement: string | string[]) => {
expect(replacement).toEqual('👍')
done()
}
})
addEmoji(emoji, editor)
})
it('1st line', done => {
Mock.extend(editor).with({
listSelections: () => (
Mock.of<Range[]>([{
anchor: firstLine.from,
head: firstLine.to,
from: () => firstLine.from,
to: () => firstLine.to,
empty: () => false
}])
),
getLine: (): string => (textFirstLine),
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
expect(from).toEqual(firstLine.from)
expect(to).toEqual(firstLine.to)
expect(replacement).toEqual('👍')
done()
}
})
addEmoji(emoji, editor)
})
it('multiple lines', done => {
Mock.extend(editor).with({
listSelections: () => (
Mock.of<Range[]>([{
anchor: multiline.from,
head: multiline.to,
from: () => multiline.from,
to: () => multiline.to,
empty: () => false
}])
),
getLine: (): string => '2nd line',
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
expect(from).toEqual(multiline.from)
expect(to).toEqual(multiline.to)
expect(replacement).toEqual('👍')
done()
}
})
addEmoji(emoji, editor)
})
it('multiple lines with offset', done => {
Mock.extend(editor).with({
listSelections: () => (
Mock.of<Range[]>([{
anchor: multilineOffset.from,
head: multilineOffset.to,
from: () => multilineOffset.from,
to: () => multilineOffset.to,
empty: () => false
}])
),
getLine: (): string => '2nd line',
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
expect(from).toEqual(multilineOffset.from)
expect(to).toEqual(multilineOffset.to)
expect(replacement).toEqual('👍')
done()
}
})
addEmoji(emoji, editor)
})
})
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 emoji = Mock.of<EmojiData>({
name: 'star',
imageUrl: '/img/forkawesome.png'
})
it('just cursor', done => {
Mock.extend(editor).with({
listSelections: () => (
Mock.of<Range[]>([{
anchor: cursor.from,
head: cursor.to,
from: () => cursor.from,
to: () => cursor.to,
empty: () => true
}])
),
getLine: (): string => (textFirstLine),
replaceRange: (replacement: string | string[]) => {
expect(replacement).toEqual(forkAwesomeIcon)
done()
}
})
addEmoji(emoji, editor)
})
it('1st line', done => {
Mock.extend(editor).with({
listSelections: () => (
Mock.of<Range[]>([{
anchor: firstLine.from,
head: firstLine.to,
from: () => firstLine.from,
to: () => firstLine.to,
empty: () => false
}])
),
getLine: (): string => (textFirstLine),
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
expect(from).toEqual(firstLine.from)
expect(to).toEqual(firstLine.to)
expect(replacement).toEqual(forkAwesomeIcon)
done()
}
})
addEmoji(emoji, editor)
})
it('multiple lines', done => {
Mock.extend(editor).with({
listSelections: () => (
Mock.of<Range[]>([{
anchor: multiline.from,
head: multiline.to,
from: () => multiline.from,
to: () => multiline.to,
empty: () => false
}])
),
getLine: (): string => '2nd line',
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
expect(from).toEqual(multiline.from)
expect(to).toEqual(multiline.to)
expect(replacement).toEqual(forkAwesomeIcon)
done()
}
})
addEmoji(emoji, editor)
})
it('multiple lines with offset', done => {
Mock.extend(editor).with({
listSelections: () => (
Mock.of<Range[]>([{
anchor: multilineOffset.from,
head: multilineOffset.to,
from: () => multilineOffset.from,
to: () => multilineOffset.to,
empty: () => false
}])
),
getLine: (): string => '2nd line',
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
expect(from).toEqual(multilineOffset.from)
expect(to).toEqual(multilineOffset.to)
expect(replacement).toEqual(forkAwesomeIcon)
done()
}
})
addEmoji(emoji, editor)
})
})

View file

@ -1,4 +1,5 @@
import { Editor } from 'codemirror'
import { BaseEmoji, CustomEmoji, EmojiData } from 'emoji-mart'
export const makeSelectionBold = (editor: Editor): void => wrapTextWith(editor, '**')
export const makeSelectionItalic = (editor: Editor): void => wrapTextWith(editor, '*')
@ -22,6 +23,17 @@ export const addLine = (editor: Editor): void => changeLines(editor, line => `${
export const addComment = (editor: Editor): void => changeLines(editor, line => `${line}\n> []`)
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).native) {
replacement = (emoji as BaseEmoji).native
} else if ((emoji as CustomEmoji).imageUrl) {
// noinspection CheckTagEmptyBody
replacement = `<i class="fa ${(emoji as CustomEmoji).name}"></i>`
}
insertAtCursor(editor, replacement)
}
export const wrapTextWith = (editor: Editor, symbol: string, endSymbol?: string): void => {
if (!editor.getSelection()) {
return
@ -98,3 +110,13 @@ export const addLink = (editor: Editor, prefix?: string): void => {
}
}
}
export const insertAtCursor = (editor: Editor, text: string): void => {
const cursor = editor.getCursor()
const ranges = editor.listSelections()
for (const range of ranges) {
const from = range.empty() ? { line: cursor.line, ch: cursor.ch } : range.from()
const to = range.empty() ? { line: cursor.line, ch: cursor.ch } : range.to()
editor.replaceRange(`${text}`, from, to, '+input')
}
}