LOTW
4 years ago
commit
1b30e4372e
41 changed files with 14348 additions and 0 deletions
@ -0,0 +1,4 @@
|
||||
{ |
||||
"presets": ["@babel/preset-env", "@babel/preset-typescript"], |
||||
"plugins": ["transform-class-properties"] |
||||
} |
@ -0,0 +1,14 @@
|
||||
root = true |
||||
|
||||
[*] |
||||
|
||||
charset = utf-8 |
||||
|
||||
# New Line formating |
||||
end_of_line = lf |
||||
insert_final_newline = true |
||||
|
||||
|
||||
# Indentation |
||||
indent_style = space |
||||
indent_size = 4 |
@ -0,0 +1,27 @@
|
||||
module.exports = { |
||||
parser: '@typescript-eslint/parser', |
||||
env: { |
||||
browser: true, |
||||
es6: true, |
||||
node: true, |
||||
jest: true |
||||
}, |
||||
extends: [ |
||||
'eslint:recommended', |
||||
'plugin:prettier/recommended', |
||||
'plugin:@typescript-eslint/eslint-recommended', |
||||
'plugin:@typescript-eslint/recommended' |
||||
], |
||||
globals: { |
||||
Atomics: 'readonly', |
||||
SharedArrayBuffer: 'readonly' |
||||
}, |
||||
parserOptions: { |
||||
ecmaVersion: 2018, |
||||
sourceType: 'module' |
||||
}, |
||||
rules: { |
||||
'no-console': 'error' |
||||
}, |
||||
plugins: ['@typescript-eslint'] |
||||
}; |
@ -0,0 +1,6 @@
|
||||
# Files |
||||
.env |
||||
.DS_Store |
||||
|
||||
# Folders |
||||
node_modules/ |
@ -0,0 +1,5 @@
|
||||
{ |
||||
"arrowParens": "avoid", |
||||
"singleQuote": true, |
||||
"trailingComma": "none" |
||||
} |
@ -0,0 +1,43 @@
|
||||
# Licenses |
||||
|
||||
* Copyright (c) 2020 LOTW. MIT |
||||
* Copyright (c) 2019 Joe Attardi. MIT |
||||
* Copyright (c) 2018 Twitter, Inc and other contributors. CC-BY-4.0 |
||||
|
||||
|
||||
## Joe Attardi's emoji-button License |
||||
This project is written off the code from Joe Attardi's emoji-button project |
||||
|
||||
* Source: https://github.com/joeattardi/emoji-button |
||||
* License: https://github.com/joeattardi/emoji-button/blob/master/LICENSE |
||||
|
||||
## Twitter Emoji for Everyone License |
||||
This project uses the emoji files from Twitter Emoji for Everyone project |
||||
|
||||
* Source: https://github.com/twitter/twemoji |
||||
* License: https://github.com/twitter/twemoji/blob/master/LICENSE |
||||
|
||||
|
||||
## Source Code |
||||
* Applies to everything else |
||||
* License: MIT |
||||
|
||||
Copyright (c) 2020 LOTW. |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,22 @@
|
||||
# Emoji Picker JS |
||||
|
||||
An Emoji Picker and parser (using twemoji) that fits for any use case. |
||||
|
||||
## Demo and Documentation |
||||
|
||||
_Coming Soon.._. |
||||
|
||||
|
||||
## Browser support |
||||
|
||||
Emoji Picker JS is supported on all modern browsers supporting the latest JavaScript features. Internet Explorer is not supported. |
||||
|
||||
|
||||
## History |
||||
|
||||
This project is basically a 'rewrite' / 'redo' of _[Joe Attardi's emoji-button project ](https://github.com/joeattardi/emoji-button)_ in which I do things the way I think or I want it to be done. |
||||
|
||||
|
||||
## License |
||||
|
||||
See [LICENSE.md](LICENSE.md) for full license info. |
@ -0,0 +1,496 @@
|
||||
@keyframes show { |
||||
0% { |
||||
opacity: 0; |
||||
transform: scale3d(0.8, 0.8, 0.8); |
||||
} |
||||
|
||||
50% { |
||||
transform: scale3d(1.05, 1.05, 1.05); |
||||
} |
||||
|
||||
100% { |
||||
transform: scale3d(1, 1, 1); |
||||
} |
||||
} |
||||
|
||||
@keyframes hide { |
||||
0% { |
||||
opacity: 1; |
||||
transform: scale3d(1, 1, 1); |
||||
} |
||||
|
||||
100% { |
||||
opacity: 0; |
||||
transform: scale3d(0.8, 0.8, 0.8); |
||||
} |
||||
} |
||||
|
||||
@keyframes grow { |
||||
0% { |
||||
opacity: 0; |
||||
transform: scale3d(0.8, 0.8, 0.8); |
||||
} |
||||
|
||||
100% { |
||||
opacity: 1; |
||||
transform: scale3d(1, 1, 1); |
||||
} |
||||
} |
||||
|
||||
@keyframes shrink { |
||||
0% { |
||||
opacity: 1; |
||||
transform: scale3d(1, 1, 1); |
||||
} |
||||
|
||||
100% { |
||||
opacity: 0; |
||||
transform: scale3d(0.8, 0.8, 0.8); |
||||
} |
||||
} |
||||
|
||||
@keyframes fade-in { |
||||
0% { opacity: 0; } |
||||
100% { opacity: 1; } |
||||
} |
||||
|
||||
@keyframes fade-out { |
||||
0% { opacity: 1; } |
||||
100% { opacity: 0; } |
||||
} |
||||
|
||||
.emoji-picker { |
||||
--animation-duration: 0.2s; |
||||
--animation-easing: ease-in-out; |
||||
|
||||
--emoji-size: 1.8em; |
||||
--emoji-size-multiplier: 1.5; |
||||
--emoji-preview-size: 2em; |
||||
--emoji-per-row: 8; |
||||
--row-count: 6; |
||||
|
||||
--content-height: calc((var(--emoji-size) * var(--emoji-size-multiplier)) * var(--row-count) + var(--category-name-size) + var(--category-button-height) + 0.5em); |
||||
|
||||
--category-name-size: 0.85em; |
||||
|
||||
--category-button-height: 2em; |
||||
--category-button-size: 1.1em; |
||||
--category-border-bottom-size: 4px; |
||||
|
||||
--focus-indicator-color: #999999; |
||||
|
||||
--search-height: 2em; |
||||
|
||||
--blue-color: #4F81E5; |
||||
|
||||
--border-color: #CCCCCC; |
||||
--background-color: #FFFFFF; |
||||
--text-color: #000000; |
||||
--secondary-text-color: #666666; |
||||
--hover-color: #E8F4F9; |
||||
--search-focus-border-color: var(--blue-color); |
||||
--search-icon-color: #CCCCCC; |
||||
--overlay-background-color: rgba(0, 0, 0, 0.8); |
||||
--popup-background-color: #FFFFFF; |
||||
--category-button-color: #666666; |
||||
--category-button-active-color: var(--blue-color); |
||||
|
||||
--dark-border-color: #666666; |
||||
--dark-background-color: #333333; |
||||
--dark-text-color: #FFFFFF; |
||||
--dark-secondary-text-color: #999999; |
||||
--dark-hover-color: #666666; |
||||
--dark-search-background-color: #666666; |
||||
--dark-search-border-color: #999999; |
||||
--dark-search-placeholder-color: #999999; |
||||
--dark-search-focus-border-color: #DBE5F9; |
||||
--dark-popup-background-color: #333333; |
||||
--dark-category-button-color: #FFFFFF; |
||||
} |
||||
|
||||
.emoji-picker__wrapper { |
||||
outline: 0; |
||||
border-radius: 5px; |
||||
} |
||||
|
||||
.emoji-picker { |
||||
font-size: 16px; |
||||
|
||||
border: 1px solid var(--border-color); |
||||
border-radius: 5px; |
||||
background: var(--background-color); |
||||
width: calc(var(--emoji-per-row) * var(--emoji-size) * var(--emoji-size-multiplier) + 1em + 1.5rem); |
||||
font-family: Arial, Helvetica, sans-serif; |
||||
overflow: hidden; |
||||
animation: show var(--animation-duration) var(--animation-easing); |
||||
} |
||||
|
||||
.emoji-picker h2 { |
||||
font-family: Arial, Helvetica, sans-serif; |
||||
} |
||||
|
||||
.emoji-picker__overlay { |
||||
background: rgba(0, 0, 0, 0.75); |
||||
z-index: 1000; |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
|
||||
.emoji-picker.hiding { |
||||
animation: hide var(--animation-duration) var(--animation-easing); |
||||
} |
||||
|
||||
.emoji-picker.dark { |
||||
background: var(--dark-background-color); |
||||
color: var(--dark-text-color); |
||||
border-color: var(--dark-border-color); |
||||
} |
||||
|
||||
.emoji-picker__content { |
||||
padding: 0.5em; |
||||
height: var(--content-height); |
||||
position: relative; |
||||
} |
||||
|
||||
.emoji-picker__preview { |
||||
height: var(--emoji-preview-size); |
||||
padding: 0.5em; |
||||
border-top: 1px solid var(--border-color); |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
} |
||||
|
||||
.emoji-picker.dark .emoji-picker__preview { |
||||
border-top-color: var(--dark-border-color); |
||||
} |
||||
|
||||
.emoji-picker__preview-emoji { |
||||
font-size: var(--emoji-preview-size); |
||||
margin-right: 0.25em; |
||||
font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "EmojiOne Color", "Android Emoji"; |
||||
} |
||||
|
||||
.emoji-picker__preview-emoji img.emoji { |
||||
height: 1em; |
||||
width: 1em; |
||||
margin: 0 .05em 0 .1em; |
||||
vertical-align: -0.1em; |
||||
} |
||||
|
||||
.emoji-picker__preview-name { |
||||
color: var(--text-color); |
||||
font-size: 0.85em; |
||||
overflow-wrap: break-word; |
||||
word-break: break-all; |
||||
} |
||||
|
||||
.emoji-picker.dark .emoji-picker__preview-name { |
||||
color: var(--dark-text-color); |
||||
} |
||||
|
||||
.emoji-picker__container { |
||||
display: grid; |
||||
justify-content: center; |
||||
grid-template-columns: repeat(var(--emoji-per-row), calc(var(--emoji-size) * var(--emoji-size-multiplier))); |
||||
grid-auto-rows: calc(var(--emoji-size) * var(--emoji-size-multiplier)); |
||||
} |
||||
|
||||
.emoji-picker__container.search-results { |
||||
height: var(--content-height); |
||||
overflow-y: auto; |
||||
} |
||||
|
||||
.emoji-picker__custom-emoji { |
||||
width: 1em; |
||||
height: 1em; |
||||
} |
||||
|
||||
.emoji-picker__emoji { |
||||
background: transparent; |
||||
border: none; |
||||
cursor: pointer; |
||||
overflow: hidden; |
||||
font-size: var(--emoji-size); |
||||
width: 1.5em; |
||||
height: 1.5em; |
||||
padding: 0; |
||||
margin: 0; |
||||
outline: none; |
||||
font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "EmojiOne Color", "Android Emoji"; |
||||
display: inline-flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
} |
||||
|
||||
.emoji-picker__emoji img.emoji { |
||||
height: 1em; |
||||
width: 1em; |
||||
margin: 0 .05em 0 .1em; |
||||
vertical-align: -0.1em; |
||||
} |
||||
|
||||
.emoji-picker__emoji:focus, .emoji-picker__emoji:hover { |
||||
background: var(--hover-color); |
||||
} |
||||
|
||||
.emoji-picker__emoji:focus { |
||||
outline: 1px dotted var(--focus-indicator-color); |
||||
} |
||||
|
||||
.emoji-picker.dark .emoji-picker__emoji:focus, .emoji-picker.dark .emoji-picker__emoji:hover { |
||||
background: var(--dark-hover-color); |
||||
} |
||||
|
||||
.emoji-picker__plugin-container { |
||||
margin: 0.5em; |
||||
display: flex; |
||||
flex-direction: row; |
||||
} |
||||
|
||||
.emoji-picker__search-container { |
||||
margin: 0.5em; |
||||
position: relative; |
||||
height: var(--search-height); |
||||
display: flex; |
||||
} |
||||
|
||||
.emoji-picker__search { |
||||
box-sizing: border-box; |
||||
width: 100%; |
||||
border-radius: 3px; |
||||
border: 1px solid var(--border-color); |
||||
padding-right: 2em; |
||||
padding: 0.5em 2.25em 0.5em 0.5em; |
||||
font-size: 0.85em; |
||||
outline: none; |
||||
} |
||||
|
||||
.emoji-picker.dark .emoji-picker__search { |
||||
background: var(--dark-search-background-color); |
||||
color: var(--dark-text-color); |
||||
border-color: var(--dark-search-border-color); |
||||
} |
||||
|
||||
.emoji-picker.dark .emoji-picker__search::placeholder { |
||||
color: var(--dark-search-placeholder-color); |
||||
} |
||||
|
||||
.emoji-picker__search:focus { |
||||
border: 1px solid var(--search-focus-border-color); |
||||
} |
||||
|
||||
.emoji-picker.dark .emoji-picker__search:focus { |
||||
border-color: var(--dark-search-focus-border-color); |
||||
} |
||||
|
||||
.emoji-picker__search-icon { |
||||
position: absolute; |
||||
color: var(--search-icon-color); |
||||
width: 1em; |
||||
height: 1em; |
||||
right: 0.75em; |
||||
top: calc(50% - 0.5em); |
||||
} |
||||
|
||||
.emoji-picker__search-icon img { |
||||
width: 1em; |
||||
height: 1em; |
||||
} |
||||
|
||||
.emoji-picker__search-not-found { |
||||
color: var(--secondary-text-color); |
||||
text-align: center; |
||||
height: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
} |
||||
|
||||
.emoji-picker__search-not-found h2 { |
||||
color: var(--secondary-text-color); |
||||
} |
||||
|
||||
.emoji-picker.dark .emoji-picker__search-not-found { |
||||
color: var(--dark-secondary-text-color); |
||||
} |
||||
|
||||
.emoji-picker.dark .emoji-picker__search-not-found h2 { |
||||
color: var(--dark-secondary-text-color); |
||||
} |
||||
|
||||
.emoji-picker__search-not-found-icon { |
||||
font-size: 3em; |
||||
} |
||||
|
||||
.emoji-picker__search-not-found-icon img { |
||||
width: 1em; |
||||
height: 1em; |
||||
} |
||||
|
||||
.emoji-picker__search-not-found h2 { |
||||
margin: 0.5em 0; |
||||
font-size: 1em; |
||||
} |
||||
|
||||
.emoji-picker__variant-overlay { |
||||
background: var(--overlay-background-color); |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
width: 100%; |
||||
height: 100%; |
||||
border-radius: 5px; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
animation: fade-in var(--animation-duration) var(--animation-easing); |
||||
} |
||||
|
||||
.emoji-picker__variant-overlay.hiding { |
||||
animation: fade-out var(--animation-duration) var(--animation-easing); |
||||
} |
||||
|
||||
.emoji-picker__variant-popup { |
||||
background: var(--popup-background-color); |
||||
margin: 0.5em; |
||||
padding: 0.5em; |
||||
text-align: center; |
||||
border-radius: 5px; |
||||
animation: grow var(--animation-duration) var(--animation-easing); |
||||
user-select: none; |
||||
} |
||||
|
||||
.emoji-picker__variant-overlay.hiding .emoji-picker__variant-popup { |
||||
animation: shrink var(--animation-duration) var(--animation-easing); |
||||
} |
||||
|
||||
.emoji-picker.dark .emoji-picker__variant-popup { |
||||
background: var(--dark-popup-background-color); |
||||
} |
||||
|
||||
.emoji-picker__emojis { |
||||
overflow-y: auto; |
||||
position: relative; |
||||
height: calc((var(--emoji-size) * var(--emoji-size-multiplier)) * var(--row-count) + var(--category-name-size)); |
||||
} |
||||
|
||||
.emoji-picker__emojis.hiding { |
||||
animation: fade-out 0.05s var(--animation-easing); |
||||
} |
||||
|
||||
.emoji-picker__emojis h2.emoji-picker__category-name { |
||||
font-size: 0.85em; |
||||
color: var(--secondary-text-color); |
||||
text-transform: uppercase; |
||||
margin: 0.25em 0; |
||||
text-align: left; |
||||
} |
||||
|
||||
.emoji-picker.dark h2.emoji-picker__category-name { |
||||
color: var(--dark-secondary-text-color); |
||||
} |
||||
|
||||
.emoji-picker__category-buttons { |
||||
display: flex; |
||||
flex-direction: row; |
||||
justify-content: space-around; |
||||
height: var(--category-button-height); |
||||
margin-bottom: 0.5em; |
||||
} |
||||
|
||||
button.emoji-picker__category-button { |
||||
flex-grow: 1; |
||||
background: transparent; |
||||
padding: 0; |
||||
border: none; |
||||
cursor: pointer; |
||||
font-size: var(--category-button-size); |
||||
vertical-align: middle; |
||||
color: var(--category-button-color); |
||||
border-bottom: var(--category-border-bottom-size) solid transparent; |
||||
outline: none; |
||||
} |
||||
|
||||
button.emoji-picker__category-button img { |
||||
width: var(--category-button-size); |
||||
height: var(--category-button-size); |
||||
} |
||||
|
||||
.emoji-picker.keyboard button.emoji-picker__category-button:focus { |
||||
outline: 1px dotted var(--focus-indicator-color); |
||||
} |
||||
|
||||
.emoji-picker.dark button.emoji-picker__category-button.active { |
||||
color: var(--category-button-active-color); |
||||
} |
||||
|
||||
.emoji-picker.dark button.emoji-picker__category-button { |
||||
color: var(--dark-category-button-color); |
||||
} |
||||
|
||||
button.emoji-picker__category-button.active { |
||||
color: var(--category-button-active-color); |
||||
border-bottom: var(--category-border-bottom-size) solid var(--category-button-active-color); |
||||
} |
||||
|
||||
@media (prefers-color-scheme: dark) { |
||||
.emoji-picker.auto { |
||||
background: var(--dark-background-color); |
||||
color: var(--dark-text-color); |
||||
border-color: var(--dark-border-color); |
||||
} |
||||
|
||||
.emoji-picker.auto .emoji-picker__preview { |
||||
border-top-color: var(--dark-border-color); |
||||
} |
||||
|
||||
.emoji-picker.auto .emoji-picker__preview-name { |
||||
color: var(--dark-text-color); |
||||
} |
||||
|
||||
.emoji-picker.auto button.emoji-picker__category-button { |
||||
color: var(--dark-category-button-color); |
||||
} |
||||
|
||||
.emoji-picker.auto button.emoji-picker__category-button.active { |
||||
color: var(--category-button-active-color); |
||||
} |
||||
|
||||
.emoji-picker.auto .emoji-picker__emoji:focus, .emoji-picker.auto .emoji-picker__emoji:hover { |
||||
background: var(--dark-hover-color); |
||||
} |
||||
|
||||
.emoji-picker.auto .emoji-picker__search { |
||||
background: var(--dark-search-background-color); |
||||
color: var(--dark-text-color); |
||||
border-color: var(--dark-search-border-color); |
||||
} |
||||
|
||||
.emoji-picker.auto h2.emoji-picker__category-name { |
||||
color: var(--dark-secondary-text-color); |
||||
} |
||||
|
||||
.emoji-picker.auto .emoji-picker__search::placeholder { |
||||
color: var(--dark-search-placeholder-color); |
||||
} |
||||
|
||||
.emoji-picker.auto .emoji-picker__search:focus { |
||||
border-color: var(--dark-search-focus-border-color); |
||||
} |
||||
|
||||
.emoji-picker.auto .emoji-picker__search-not-found { |
||||
color: var(--dark-secondary-text-color); |
||||
} |
||||
|
||||
.emoji-picker.auto .emoji-picker__search-not-found h2 { |
||||
color: var(--dark-secondary-text-color); |
||||
} |
||||
|
||||
.emoji-picker.auto .emoji-picker__variant-popup { |
||||
background: var(--dark-popup-background-color); |
||||
} |
||||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,173 @@
|
||||
export as namespace EmojiPicker; |
||||
|
||||
export = EmojiPicker; |
||||
|
||||
declare namespace EmojiPicker { |
||||
export class EmojiPicker { |
||||
|
||||
// Constructor Function
|
||||
constructor(options?: EmojiPicker.Options); |
||||
|
||||
// on event listener
|
||||
on(event: Event, callback: (selection: EmojiSelection) => void): void; |
||||
|
||||
// off event listener
|
||||
off(event: Event, callback: (selection: EmojiSelection) => void): void; |
||||
|
||||
// returns an emoji from twemoji based on an emoji input
|
||||
getEmoji(emoji: String): Object; |
||||
|
||||
// takes in html string and parses it together with the emoji
|
||||
parse(htmlString: String): String; |
||||
|
||||
// hidePicker function
|
||||
hidePicker(): void; |
||||
|
||||
// destroyPicker function
|
||||
destroyPicker(): void; |
||||
|
||||
// showPicker function
|
||||
showPicker(referenceEl: HTMLElement): void; |
||||
|
||||
// togglePicker function
|
||||
togglePicker(referenceEl: HTMLElement): void; |
||||
|
||||
// isPickerVisible function
|
||||
isPickerVisible(): boolean; |
||||
|
||||
// setTheme function
|
||||
setTheme(theme: EmojiTheme): void; |
||||
} |
||||
|
||||
export interface Options { |
||||
position?: Placement | FixedPosition; |
||||
autoHide?: boolean; |
||||
autoFocusSearch?: boolean; |
||||
showAnimation?: boolean; |
||||
showPreview?: boolean; |
||||
showSearch?: boolean; |
||||
showRecents?: boolean; |
||||
showVariants?: boolean; |
||||
showCategoryButtons?: boolean; |
||||
recentsCount?: number; |
||||
emojiVersion?: EmojiVersion; |
||||
i18n?: I18NStrings; |
||||
zIndex?: number; |
||||
boxShadow?: string | 'none'; |
||||
theme?: EmojiTheme; |
||||
categories?: Category[]; |
||||
style?: EmojiStyle; |
||||
twemojiBaseUrl?: string; |
||||
emojisPerRow?: number; |
||||
rows?: number; |
||||
emojiSize?: string; |
||||
initialCategory?: Category | 'recents'; |
||||
custom?: CustomEmoji[]; |
||||
plugins?: Plugin[]; |
||||
icons?: Icons; |
||||
rootElement?: HTMLElement; |
||||
} |
||||
|
||||
export interface TwemojiOptions { |
||||
base?: string, |
||||
ext: string, |
||||
folder: string |
||||
} |
||||
|
||||
export interface FixedPosition { |
||||
top?: string; |
||||
bottom?: string; |
||||
left?: string; |
||||
right?: string; |
||||
} |
||||
|
||||
export interface Plugin { |
||||
render(picker: EmojiPicker): HTMLElement; |
||||
destroy?(): void; |
||||
} |
||||
|
||||
export interface EmojiSelection { |
||||
custom?: boolean; |
||||
emoji?: string; |
||||
url?: string; |
||||
} |
||||
|
||||
export interface CustomEmoji { |
||||
name: string; |
||||
emoji: string; |
||||
} |
||||
|
||||
export type EmojiStyle = 'native' | 'twemoji'; |
||||
|
||||
export type EmojiTheme = 'dark' | 'light' | 'auto'; |
||||
|
||||
export type Event = 'emoji' | 'hidden'; |
||||
|
||||
export type Placement = |
||||
| 'auto' |
||||
| 'auto-start' |
||||
| 'auto-end' |
||||
| 'top' |
||||
| 'top-start' |
||||
| 'top-end' |
||||
| 'bottom' |
||||
| 'bottom-start' |
||||
| 'bottom-end' |
||||
| 'right' |
||||
| 'right-start' |
||||
| 'right-end' |
||||
| 'left' |
||||
| 'left-start' |
||||
| 'left-end'; |
||||
|
||||
export type EmojiVersion = |
||||
| '1.0' |
||||
| '2.0' |
||||
| '3.0' |
||||
| '4.0' |
||||
| '5.0' |
||||
| '11.0' |
||||
| '12.0' |
||||
| '12.1'; |
||||
|
||||
export type Category = |
||||
| 'smileys' |
||||
| 'people' |
||||
| 'animals' |
||||
| 'food' |
||||
| 'activities' |
||||
| 'travel' |
||||
| 'objects' |
||||
| 'symbols' |
||||
| 'flags'; |
||||
|
||||
export type I18NCategory = |
||||
| 'recents' |
||||
| 'smileys' |
||||
| 'people' |
||||
| 'animals' |
||||
| 'food' |
||||
| 'activities' |
||||
| 'travel' |
||||
| 'objects' |
||||
| 'symbols' |
||||
| 'flags' |
||||
| 'custom'; |
||||
|
||||
export interface I18NStrings { |
||||
search: string; |
||||
categories: { |
||||
[key in I18NCategory]: string; |
||||
}; |
||||
notFound: string; |
||||
} |
||||
|
||||
export interface Icons { |
||||
search?: string; |
||||
clearSearch?: string; |
||||
categories?: { |
||||
[key in I18NCategory]?: string; |
||||
}; |
||||
notFound?: string; |
||||
} |
||||
} |
@ -0,0 +1,27 @@
|
||||
name: Node CI |
||||
|
||||
on: [push] |
||||
|
||||
jobs: |
||||
build: |
||||
|
||||
runs-on: ubuntu-latest |
||||
|
||||
strategy: |
||||
matrix: |
||||
node-version: [8.x, 10.x, 12.x] |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v1 |
||||
- name: Use Node.js ${{ matrix.node-version }} |
||||
uses: actions/setup-node@v1 |
||||
with: |
||||
node-version: ${{ matrix.node-version }} |
||||
- name: npm install, build, lint, and test |
||||
run: | |
||||
npm install |
||||
npm run build |
||||
npm run lint |
||||
npm test |
||||
env: |
||||
CI: true |
@ -0,0 +1,67 @@
|
||||
{ |
||||
"name": "emoji-picker-js", |
||||
"version": "0.1.0", |
||||
"description": "An Emoji Picker and Parser", |
||||
"keywords": [ |
||||
"emoji", |
||||
"javascript" |
||||
], |
||||
"main": "dist/index.js", |
||||
"module": "dist/index.js", |
||||
"types": "index.d.ts", |
||||
"scripts": { |
||||
"build": "cross-env NODE_ENV=production rollup -c", |
||||
"watch": "rollup -cw", |
||||
"test": "jest src/**.test.ts", |
||||
"test:watch": "jest --watchAll", |
||||
"lint": "eslint src/*.ts", |
||||
"prettify": "prettier src/*.ts --write" |
||||
}, |
||||
"author": "LOTW <[email protected]>", |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "https://github.com/lotw7277/emoji-picker-js.git" |
||||
}, |
||||
"license": "MIT", |
||||
"devDependencies": { |
||||
"@babel/core": "^7.10.2", |
||||
"@babel/preset-env": "^7.10.2", |
||||
"@babel/preset-typescript": "^7.10.1", |
||||
"@rollup/plugin-replace": "^2.3.3", |
||||
"@rollup/plugin-typescript": "^4.1.2", |
||||
"@types/jest": "^25.2.3", |
||||
"@typescript-eslint/eslint-plugin": "^3.1.0", |
||||
"@typescript-eslint/parser": "^3.1.0", |
||||
"babel-jest": "^26.0.1", |
||||
"babel-plugin-transform-class-properties": "^6.24.1", |
||||
"cross-env": "^7.0.2", |
||||
"eslint": "^7.2.0", |
||||
"eslint-config-prettier": "^6.11.0", |
||||
"eslint-plugin-prettier": "^3.1.3", |
||||
"jest": "^26.0.1", |
||||
"prettier": "^2.0.5", |
||||
"rollup": "^2.22.1", |
||||
"rollup-plugin-babel": "^4.4.0", |
||||
"rollup-plugin-commonjs": "^10.1.0", |
||||
"rollup-plugin-node-resolve": "^5.2.0", |
||||
"rollup-plugin-postcss": "^3.1.2", |
||||
"rollup-plugin-terser": "^6.1.0", |
||||
"ts-jest": "^26.1.0", |
||||
"typescript": "^3.9.5" |
||||
}, |
||||
"dependencies": { |
||||
"@fortawesome/fontawesome-svg-core": "^1.2.28", |
||||
"@fortawesome/free-regular-svg-icons": "^5.13.0", |
||||
"@fortawesome/free-solid-svg-icons": "^5.13.0", |
||||
"@popperjs/core": "^2.4.0", |
||||
"focus-trap": "^5.1.0", |
||||
"fuzzysort": "^1.1.4", |
||||
"tiny-emitter": "^2.1.0", |
||||
"tslib": "^2.0.0", |
||||
"twemoji": "^13.0.1" |
||||
}, |
||||
"files": [ |
||||
"dist", |
||||
"index.d.ts" |
||||
] |
||||
} |
@ -0,0 +1,34 @@
|
||||
import commonjs from 'rollup-plugin-commonjs'; |
||||
import postcss from 'rollup-plugin-postcss'; |
||||
import resolve from 'rollup-plugin-node-resolve'; |
||||
import replace from '@rollup/plugin-replace'; |
||||
import typescript from '@rollup/plugin-typescript'; |
||||
import { terser } from 'rollup-plugin-terser'; |
||||
|
||||
const production = process.env.NODE_ENV === 'production'; |
||||
|
||||
export default { |
||||
input: 'src/index.ts', |
||||
output: { |
||||
file: 'dist/index.js', |
||||
format: 'es', |
||||
name: 'EmojiPicker' |
||||
}, |
||||
watch: { |
||||
buildDelay: 500 |
||||
}, |
||||
plugins: [ |
||||
replace({ |
||||
'process.env.NODE_ENV': JSON.stringify( |
||||
production ? 'production' : 'development' |
||||
) |
||||
}), |
||||
postcss({ |
||||
extensions: ['.css'] |
||||
}), |
||||
typescript(), |
||||
resolve(), |
||||
commonjs(), |
||||
production && terser() |
||||
] |
||||
}; |
@ -0,0 +1,143 @@
|
||||
const fs = require('fs'); |
||||
const readline = require('readline'); |
||||
|
||||
const DATA_LINE_REGEX = /((?:[0-9A-F]+ ?)+)\s+;(.+)\s+#.+E([0-9.]+) (.+)/; |
||||
const EMOJI_WITH_MODIFIER_REGEX = /([a-z]+): ([a-z -]+)/; |
||||
const EMOJI_WITH_SKIN_TONE_AND_MODIFIER_REGEX = /([a-z]+): ([a-z -]+), ([a-z ]+)/; |
||||
|
||||
const categoryKeys = { |
||||
'Smileys & Emotion': 'smileys', |
||||
'People & Body': 'people', |
||||
'Animals & Nature': 'animals', |
||||
'Food & Drink': 'food', |
||||
'Travel & Places': 'travel', |
||||
'Activities': 'activities', |
||||
'Objects': 'objects', |
||||
'Symbols': 'symbols', |
||||
'Flags': 'flags' |
||||
}; |
||||
|
||||
const BLACKLIST = [ |
||||
'light skin tone', |
||||
'medium-light skin tone', |
||||
'medium skin tone', |
||||
'medium-dark skin tone', |
||||
'dark skin tone', |
||||
'red hair', |
||||
'white hair', |
||||
'curly hair', |
||||
'bald' |
||||
]; |
||||
|
||||
const MODIFIER_SUBSTITUTIONS = { |
||||
'bald': 'no hair' |
||||
}; |
||||
|
||||
const stream = fs.createReadStream('emoji-test.txt'); |
||||
|
||||
const interface = readline.createInterface(stream); |
||||
|
||||
let currentGroup; |
||||
let currentSubgroup; |
||||
let categoryIndex; |
||||
|
||||
const data = { |
||||
categories: [], |
||||
emoji: [] |
||||
}; |
||||
|
||||
interface.on('line', line => { |
||||
if (line.startsWith('# group:')) { |
||||
currentGroup = line.slice('# group: '.length); |
||||
if (currentGroup !== 'Component') { |
||||
data.categories.push(categoryKeys[currentGroup]); |
||||
categoryIndex = data.categories.length - 1; |
||||
} |
||||
} else if (line.startsWith('# subgroup:')) { |
||||
currentSubgroup = line.slice('# subgroup: '.length); |
||||
} else if (!line.startsWith('#') && currentGroup !== 'Component') { |
||||
const matcher = DATA_LINE_REGEX.exec(line); |
||||
if (matcher) { |
||||
const sequence = matcher[1].trim(); |
||||
const emoji = getEmoji(sequence); |
||||
let name = matcher[4]; |
||||
|
||||
let version = matcher[3]; |
||||
if (version === '0.6' || version === '0.7') { |
||||
version = '1.0'; |
||||
} |
||||
|
||||
if (currentSubgroup === 'person') { |
||||
const modifierMatcher = EMOJI_WITH_MODIFIER_REGEX.exec(name); |
||||
const skinToneMatcher = EMOJI_WITH_SKIN_TONE_AND_MODIFIER_REGEX.exec(name); |
||||
if (skinToneMatcher) { |
||||
name = skinToneMatcher[1] + ' with ' + substituteModifier(skinToneMatcher[3]) + ': ' + skinToneMatcher[2]; |
||||
} else if (modifierMatcher) { |
||||
if (!modifierMatcher[2].includes('skin tone')) { |
||||
name = modifierMatcher[1] + ' with ' + substituteModifier(modifierMatcher[2]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (matcher[2].trim() !== 'unqualified') { |
||||
data.emoji.push({ sequence, emoji, category: categoryIndex, name, variations: [], version }); |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
|
||||
interface.on('close', () => { |
||||
stream.close(); |
||||
|
||||
let toDelete = []; |
||||
|
||||
const emojisWithVariationSelector = data.emoji.filter(emoji => emoji.sequence.includes('FE0F')); |
||||
emojisWithVariationSelector.forEach(emoji => { |
||||
const baseEmoji = data.emoji.find(e => e.sequence === emoji.sequence.replace(' FE0F', '')); |
||||
toDelete.push(baseEmoji); |
||||
}); |
||||
|
||||
data.emoji = data.emoji.filter(e => !toDelete.includes(e)); |
||||
toDelete = []; |
||||
|
||||
BLACKLIST.forEach(name => toDelete.push(data.emoji.find(e => e.name === name))); |
||||
|
||||
const emojisWithVariations = data.emoji.filter(emoji => emoji.name.includes(':') && !emoji.name.startsWith('family')); |
||||
emojisWithVariations.forEach(emoji => { |
||||
const baseName = emoji.name.split(':')[0]; |
||||
const baseEmoji = data.emoji.find(e => e.name === baseName); |
||||
if (baseEmoji) { |
||||
baseEmoji.variations.push(emoji.emoji); |
||||
toDelete.push(emoji); |
||||
} |
||||
}); |
||||
|
||||
// Cleanup
|
||||
data.emoji = data.emoji.filter(e => !toDelete.includes(e)); |
||||
data.emoji.forEach(emoji => { |
||||
delete emoji.sequence; |
||||
if (!emoji.variations.length) { |
||||
delete emoji.variations; |
||||
} |
||||
}); |
||||
|
||||
fs.writeFileSync('src/data/emoji.js', `export default ${JSON.stringify(data)}`); |
||||
}); |
||||
|
||||
function getEmoji(sequence) { |
||||
const chars = sequence.split(' '); |
||||
const codePoints = chars.map(char => parseInt(char, 16)); |
||||
return String.fromCodePoint(...codePoints); |
||||
} |
||||
|
||||
function substituteModifier(name) { |
||||
const substitutions = Object.keys(MODIFIER_SUBSTITUTIONS); |
||||
for (let i = 0; i < substitutions.length; i++) { |
||||
const substitution = substitutions[i]; |
||||
if (name.includes(substitution)) { |
||||
return name.replace(substitution, MODIFIER_SUBSTITUTIONS[substitution]); |
||||
} |
||||
} |
||||
|
||||
return name; |
||||
} |
@ -0,0 +1,71 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import emojiData from './data/emoji'; |
||||
|
||||
import { CategoryButtons } from './categoryButtons'; |
||||
import { i18n } from './i18n'; |
||||
|
||||
const emitter = new Emitter(); |
||||
|
||||
describe('CategoryButtons', () => { |
||||
test('should render all categories if no categories are specified', () => { |
||||
const container = new CategoryButtons({}, emitter, i18n).render(); |
||||
|
||||
const buttons = container.querySelectorAll('button'); |
||||
expect(buttons).toHaveLength(emojiData.categories.length); |
||||
buttons.forEach((button, index) => { |
||||
expect(button.title).toEqual( |
||||
i18n.categories[emojiData.categories[index]] |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
test('should include the recents category if showRecents is true', () => { |
||||
const container = new CategoryButtons( |
||||
{ showRecents: true }, |
||||
emitter, |
||||
i18n |
||||
).render(); |
||||
|
||||
const buttons = container.querySelectorAll('button'); |
||||
expect(buttons).toHaveLength(emojiData.categories.length + 1); |
||||
expect(buttons[0].title).toEqual(i18n.categories.recents); |
||||
Array.prototype.slice.call(buttons, 1).forEach((button, index) => { |
||||
expect(button.title).toEqual( |
||||
i18n.categories[emojiData.categories[index]] |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
test('should only render specified categories if they are specified', () => { |
||||
const container = new CategoryButtons( |
||||
{ |
||||
categories: ['smileys', 'animals'] |
||||
}, |
||||
emitter, |
||||
i18n |
||||
).render(); |
||||
|
||||
const buttons = container.querySelectorAll('button'); |
||||
expect(buttons).toHaveLength(2); |
||||
expect(buttons[0].title).toEqual(i18n.categories.smileys); |
||||
expect(buttons[1].title).toEqual(i18n.categories.animals); |
||||
}); |
||||
|
||||
test('should include the recents with filtered categories if showRecents is true', () => { |
||||
const container = new CategoryButtons( |
||||
{ |
||||
categories: ['smileys', 'animals'], |
||||
showRecents: true |
||||
}, |
||||
emitter, |
||||
i18n |
||||
).render(); |
||||
|
||||
const buttons = container.querySelectorAll('button'); |
||||
expect(buttons).toHaveLength(3); |
||||
expect(buttons[0].title).toEqual(i18n.categories.recents); |
||||
expect(buttons[1].title).toEqual(i18n.categories.smileys); |
||||
expect(buttons[2].title).toEqual(i18n.categories.animals); |
||||
}); |
||||
}); |
@ -0,0 +1,118 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import { CLASS_CATEGORY_BUTTONS, CLASS_CATEGORY_BUTTON } from './classes'; |
||||
|
||||
import emojiData from './data/emoji'; |
||||
|
||||
import { CATEGORY_CLICKED } from './events'; |
||||
|
||||
import * as icons from './icons'; |
||||
import { createElement } from './util'; |
||||
|
||||
import { EmojiPickerOptions, I18NCategory, I18NStrings } from './types'; |
||||
|
||||
const categoryIcons: { [key in I18NCategory]: string } = { |
||||
recents: icons.history, |
||||
smileys: icons.smile, |
||||
people: icons.user, |
||||
animals: icons.cat, |
||||
food: icons.coffee, |
||||
activities: icons.futbol, |
||||
travel: icons.building, |
||||
objects: icons.lightbulb, |
||||
symbols: icons.music, |
||||
flags: icons.flag, |
||||
custom: icons.icons |
||||
}; |
||||
|
||||
export class CategoryButtons { |
||||
constructor( |
||||
private options: EmojiPickerOptions, |
||||
private events: Emitter, |
||||
private i18n: I18NStrings |
||||
) { } |
||||
|
||||
activeButton = 0; |
||||
|
||||
buttons: HTMLElement[] = []; |
||||
|
||||
render(): HTMLElement { |
||||
const container = createElement('div', CLASS_CATEGORY_BUTTONS); |
||||
|
||||
let categories = this.options.showRecents |
||||
? ['recents', ...(this.options.categories || emojiData.categories)] |
||||
: this.options.categories || emojiData.categories; |
||||
|
||||
if (this.options.custom) { |
||||
categories = [...categories, 'custom']; |
||||
} |
||||
|
||||
categories.forEach((category: string) => { |
||||
const button = createElement('button', CLASS_CATEGORY_BUTTON); |
||||
|
||||
if ( |
||||
this.options.icons && |
||||
this.options.icons.categories && |
||||
this.options.icons.categories[category] |
||||
) { |
||||
button.appendChild( |
||||
icons.createIcon(this.options.icons.categories[category]) |
||||
); |
||||
} else { |
||||
button.innerHTML = categoryIcons[category]; |
||||
} |
||||
|
||||
button.tabIndex = -1; |
||||
button.title = this.i18n.categories[category]; |
||||
container.appendChild(button); |
||||
this.buttons.push(button); |
||||
|
||||
button.addEventListener('click', () => { |
||||
this.events.emit(CATEGORY_CLICKED, category); |
||||
}); |
||||
}); |
||||
|
||||
container.addEventListener('keydown', event => { |
||||
switch (event.key) { |
||||
case 'ArrowRight': |
||||
this.events.emit( |
||||
CATEGORY_CLICKED, |
||||
categories[(this.activeButton + 1) % this.buttons.length] |
||||
); |
||||
break; |
||||
case 'ArrowLeft': |
||||
this.events.emit( |
||||
CATEGORY_CLICKED, |
||||
categories[ |
||||
this.activeButton === 0 |
||||
? this.buttons.length - 1 |
||||
: this.activeButton - 1 |
||||
] |
||||
); |
||||
break; |
||||
case 'ArrowUp': |
||||
case 'ArrowDown': |
||||
event.stopPropagation(); |
||||
event.preventDefault(); |
||||
} |
||||
}); |
||||
|
||||
return container; |
||||
} |
||||
|
||||
setActiveButton(activeButton: number, focus = true): void { |
||||
let activeButtonEl = this.buttons[this.activeButton]; |
||||
activeButtonEl.classList.remove('active'); |
||||
activeButtonEl.tabIndex = -1; |
||||
|
||||
this.activeButton = activeButton; |
||||
|
||||
activeButtonEl = this.buttons[this.activeButton]; |
||||
activeButtonEl.classList.add('active'); |
||||
activeButtonEl.tabIndex = 0; |
||||
|
||||
if (focus) { |
||||
activeButtonEl.focus(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
export const CLASS_CATEGORY_BUTTON = 'emoji-picker__category-button'; |
||||
export const CLASS_CATEGORY_BUTTONS = 'emoji-picker__category-buttons'; |
||||
export const CLASS_CATEGORY_NAME = 'emoji-picker__category-name'; |
||||
export const CLASS_CUSTOM_EMOJI = 'emoji-picker__custom-emoji'; |
||||
export const CLASS_EMOJI = 'emoji-picker__emoji'; |
||||
export const CLASS_EMOJI_AREA = 'emoji-picker__emoji-area'; |
||||
export const CLASS_EMOJI_CONTAINER = 'emoji-picker__container'; |
||||
export const CLASS_EMOJIS = 'emoji-picker__emojis'; |
||||
export const CLASS_NOT_FOUND = 'emoji-picker__search-not-found'; |
||||
export const CLASS_NOT_FOUND_ICON = 'emoji-picker__search-not-found-icon'; |
||||
export const CLASS_OVERLAY = 'emoji-picker__overlay'; |
||||
export const CLASS_PICKER = 'emoji-picker'; |
||||
export const CLASS_PICKER_CONTENT = 'emoji-picker__content'; |
||||
export const CLASS_PLUGIN_CONTAINER = 'emoji-picker__plugin-container'; |
||||
export const CLASS_PREVIEW = 'emoji-picker__preview'; |
||||
export const CLASS_PREVIEW_EMOJI = 'emoji-picker__preview-emoji'; |
||||
export const CLASS_PREVIEW_NAME = 'emoji-picker__preview-name'; |
||||
export const CLASS_SEARCH_CONTAINER = 'emoji-picker__search-container'; |
||||
export const CLASS_SEARCH_FIELD = 'emoji-picker__search'; |
||||
export const CLASS_SEARCH_ICON = 'emoji-picker__search-icon'; |
||||
export const CLASS_VARIANT_OVERLAY = 'emoji-picker__variant-overlay'; |
||||
export const CLASS_VARIANT_POPUP = 'emoji-picker__variant-popup'; |
||||
export const CLASS_WRAPPER = 'emoji-picker__wrapper'; |
File diff suppressed because one or more lines are too long
@ -0,0 +1,65 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import { EMOJI, HIDE_PREVIEW, SHOW_PREVIEW } from './events'; |
||||
import { Emoji } from './emoji'; |
||||
import { EmojiPickerOptions } from './types'; |
||||
|
||||
describe('Emoji', () => { |
||||
let events; |
||||
|
||||
const testEmoji = { |
||||
emoji: '😄', |
||||
name: 'smile', |
||||
category: 0, |
||||
version: '11.0' |
||||
}; |
||||
const options: EmojiPickerOptions = { showRecents: true, style: 'native' }; |
||||
|
||||
beforeEach(() => (events = new Emitter())); |
||||
|
||||
test('should render the emoji', () => { |
||||
const emoji = new Emoji(testEmoji, false, false, events, options); |
||||
const element = emoji.render(); |
||||
|
||||
expect(element.innerHTML).toEqual(testEmoji.emoji); |
||||
}); |
||||
|
||||
test('should emit the EMOJI event when clicked', done => { |
||||
const emoji = new Emoji(testEmoji, false, false, events, options); |
||||
const element = emoji.render(); |
||||
|
||||
events.on(EMOJI, e => { |
||||
expect(e).toEqual({ |
||||
emoji: testEmoji, |
||||
showVariants: false, |
||||
button: element |
||||
}); |
||||
done(); |
||||
}); |
||||
|
||||
element.dispatchEvent(new MouseEvent('click')); |
||||
}); |
||||
|
||||
test('should emit the SHOW_PREVIEW event on mouseover if showPreview is true', done => { |
||||
const emoji = new Emoji(testEmoji, false, true, events, options); |
||||
const element = emoji.render(); |
||||
|
||||
events.on(SHOW_PREVIEW, e => { |
||||
expect(e).toEqual(testEmoji); |
||||
done(); |
||||
}); |
||||
|
||||
element.dispatchEvent(new MouseEvent('mouseover')); |
||||
}); |
||||
|
||||
test('should emit the HIDE_PREVIEW event on mouseout if showPreview is true', done => { |
||||
const emoji = new Emoji(testEmoji, false, true, events, options); |
||||
const element = emoji.render(); |
||||
|
||||
events.on(HIDE_PREVIEW, () => { |
||||
done(); |
||||
}); |
||||
|
||||
element.dispatchEvent(new MouseEvent('mouseout')); |
||||
}); |
||||
}); |
@ -0,0 +1,131 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
import twemoji from 'twemoji'; |
||||
|
||||
import { EMOJI, HIDE_PREVIEW, SHOW_PREVIEW } from './events'; |
||||
import { smile } from './icons'; |
||||
import { save } from './recent'; |
||||
import { createElement } from './util'; |
||||
|
||||
import { CLASS_EMOJI, CLASS_CUSTOM_EMOJI } from './classes'; |
||||
|
||||
import { EmojiPickerOptions, EmojiRecord, TwemojiOptions } from './types'; |
||||
|
||||
const DEFAULT_TWEMOJI_OPTIONS: TwemojiOptions = { |
||||
ext: '.svg', |
||||
folder: 'svg' |
||||
}; |
||||
|
||||
export class Emoji { |
||||
private EmojiPicker: HTMLElement; |
||||
|
||||
private emoji: EmojiRecord; |
||||
private showVariants: boolean; |
||||
private showPreview: boolean; |
||||
private events: Emitter; |
||||
private options: EmojiPickerOptions; |
||||
private twOptions: TwemojiOptions; |
||||
private lazy = true; |
||||
|
||||
constructor( |
||||
emoji: EmojiRecord, |
||||
showVariants: boolean, |
||||
showPreview: boolean, |
||||
events: Emitter, |
||||
options: EmojiPickerOptions, |
||||
lazy = true |
||||
) { |
||||
this.emoji = emoji |
||||
this.showVariants = showVariants |
||||
this.showPreview = showPreview |
||||
this.events = events |
||||
this.options = options |
||||
this.lazy = lazy |
||||
|
||||
// Check for twemojiBaseUrl, if present add to the default options
|
||||
options.twemojiBaseUrl ? this.twOptions = { ...DEFAULT_TWEMOJI_OPTIONS, base: options.twemojiBaseUrl } : this.twOptions = { ...DEFAULT_TWEMOJI_OPTIONS } |
||||
|
||||
|
||||
} |
||||
|
||||
render(): HTMLElement { |
||||
this.EmojiPicker = createElement('button', CLASS_EMOJI); |
||||
|
||||
let content = this.emoji.emoji; |
||||
|
||||
/* |
||||
const img = createElement( |
||||
'img', |
||||
CLASS_CUSTOM_EMOJI |
||||
) as HTMLImageElement; |
||||
img.src = element.dataset.emoji; |
||||
element.innerText = ''; |
||||
element.appendChild(img); |
||||
element.dataset.loaded = true; |
||||
element.style.opacity = 1; |
||||
*/ |
||||
|
||||
if (this.emoji.custom) { |
||||
content = this.lazy |
||||
? smile |
||||
: `<img class="${CLASS_CUSTOM_EMOJI}" src="${this.emoji.emoji}">`; |
||||
} else if (this.options.style === 'twemoji') { |
||||
content = this.lazy ? smile : twemoji.parse(this.emoji.emoji, this.twOptions); |
||||
} |
||||
|
||||
this.EmojiPicker.innerHTML = content; |
||||
// this.options.style === 'native'
|
||||
// ? this.emoji.emoji
|
||||
// : this.lazy
|
||||
// ? smile
|
||||
// : twemoji.parse(this.emoji.emoji);
|
||||
this.EmojiPicker.tabIndex = -1; |
||||
|
||||
this.EmojiPicker.dataset.emoji = this.emoji.emoji; |
||||
if (this.emoji.custom) { |
||||
this.EmojiPicker.dataset.custom = 'true'; |
||||
} |
||||
this.EmojiPicker.title = this.emoji.name; |
||||
|
||||
this.EmojiPicker.addEventListener('focus', () => this.onEmojiHover()); |
||||
this.EmojiPicker.addEventListener('blur', () => this.onEmojiLeave()); |
||||
this.EmojiPicker.addEventListener('click', () => this.onEmojiClick()); |
||||
this.EmojiPicker.addEventListener('mouseover', () => this.onEmojiHover()); |
||||
this.EmojiPicker.addEventListener('mouseout', () => this.onEmojiLeave()); |
||||
|
||||
if (this.options.style === 'twemoji' && this.lazy) { |
||||
this.EmojiPicker.style.opacity = '0.25'; |
||||
} |
||||
|
||||
return this.EmojiPicker; |
||||
} |
||||
|
||||
onEmojiClick(): void { |
||||
// TODO move this side effect out of Emoji, make the recent module listen for event
|
||||
if ( |
||||
(!(this.emoji as EmojiRecord).variations || |
||||
!this.showVariants || |
||||
!this.options.showVariants) && |
||||
this.options.showRecents |
||||
) { |
||||
save(this.emoji, this.options); |
||||
} |
||||
|
||||
this.events.emit(EMOJI, { |
||||
emoji: this.emoji, |
||||
showVariants: this.showVariants, |
||||
button: this.EmojiPicker |
||||
}); |
||||
} |
||||
|
||||
onEmojiHover(): void { |
||||
if (this.showPreview) { |
||||
this.events.emit(SHOW_PREVIEW, this.emoji); |
||||
} |
||||
} |
||||
|
||||
onEmojiLeave(): void { |
||||
if (this.showPreview) { |
||||
this.events.emit(HIDE_PREVIEW); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,84 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import emojiData from './data/emoji'; |
||||
|
||||
import { EmojiArea } from './emojiArea'; |
||||
import { i18n } from './i18n'; |
||||
|
||||
const emitter = new Emitter(); |
||||
|
||||
describe('EmojiArea', () => { |
||||
test('renders an emoji list for each category', () => { |
||||
const emojiArea = new EmojiArea(emitter, i18n, { |
||||
emojiVersion: '11.0' |
||||
}).render(); |
||||
|
||||
const containers = emojiArea.querySelectorAll('.emoji-picker__container'); |
||||
expect(containers).toHaveLength(emojiData.categories.length); |
||||
|
||||
const names = emojiArea.querySelectorAll('h2'); |
||||
expect(names).toHaveLength(emojiData.categories.length); |
||||
names.forEach((name, index) => { |
||||
expect(name.innerHTML.replace('&', '&')).toEqual( |
||||
i18n.categories[emojiData.categories[index]] |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
test('only renders emoji lists for specified categories', () => { |
||||
const emojiArea = new EmojiArea(emitter, i18n, { |
||||
emojiVersion: '11.0', |
||||
categories: ['smileys', 'animals'] |
||||
}).render(); |
||||
|
||||
const containers = emojiArea.querySelectorAll('.emoji-picker__container'); |
||||
expect(containers).toHaveLength(2); |
||||
|
||||
const names = emojiArea.querySelectorAll('h2'); |
||||
expect(names).toHaveLength(2); |
||||
expect(names[0].innerHTML.replace('&', '&')).toEqual( |
||||
i18n.categories.smileys |
||||
); |
||||
expect(names[1].innerHTML.replace('&', '&')).toEqual( |
||||
i18n.categories.animals |
||||
); |
||||
}); |
||||
|
||||
test('includes the recents category if showRecents is true', () => { |
||||
const emojiArea = new EmojiArea(emitter, i18n, { |
||||
emojiVersion: '11.0', |
||||
categories: ['smileys', 'animals'], |
||||
showRecents: true |
||||
}).render(); |
||||
|
||||
const containers = emojiArea.querySelectorAll('.emoji-picker__container'); |
||||
expect(containers).toHaveLength(3); |
||||
|
||||
const names = emojiArea.querySelectorAll('h2'); |
||||
expect(names).toHaveLength(3); |
||||
expect(names[0].innerHTML).toEqual(i18n.categories.recents); |
||||
expect(names[1].innerHTML.replace('&', '&')).toEqual( |
||||
i18n.categories.smileys |
||||
); |
||||
expect(names[2].innerHTML.replace('&', '&')).toEqual( |
||||
i18n.categories.animals |
||||
); |
||||
}); |
||||
|
||||
test('selects the initial category', () => { |
||||
const emojiArea = new EmojiArea(emitter, i18n, { |
||||
emojiVersion: '11.0', |
||||
categories: ['smileys', 'animals'], |
||||
showRecents: true, |
||||
initialCategory: 'animals', |
||||
showCategoryButtons: true |
||||
}); |
||||
const container = emojiArea.render(); |
||||
emojiArea.reset(); |
||||
|
||||
const buttons = container.querySelectorAll( |
||||
'.emoji-picker__category-button' |
||||
); |
||||
expect(buttons[2].classList).toContain('active'); |
||||
}); |
||||
}); |
@ -0,0 +1,347 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import emojiData from './data/emoji'; |
||||
import { i18n as defaultI18n } from './i18n'; |
||||
|
||||
import { CLASS_EMOJI_CONTAINER, CLASS_EMOJI, CLASS_EMOJI_AREA, CLASS_EMOJIS, CLASS_CATEGORY_NAME } from './classes'; |
||||
|
||||
import { CategoryButtons } from './categoryButtons'; |
||||
import { EmojiContainer } from './emojiContainer'; |
||||
|
||||
import { CATEGORY_CLICKED } from './events'; |
||||
import { I18NStrings, EmojiPickerOptions, EmojiRecord, RecentEmoji } from './types'; |
||||
import { createElement } from './util'; |
||||
import { load } from './recent'; |
||||
|
||||
const emojiCategories: { [key: string]: EmojiRecord[] } = {}; |
||||
emojiData.emoji.forEach(emoji => { |
||||
let categoryList = emojiCategories[emojiData.categories[emoji.category]]; |
||||
if (!categoryList) { |
||||
categoryList = emojiCategories[emojiData.categories[emoji.category]] = []; |
||||
} |
||||
|
||||
categoryList.push(emoji); |
||||
}); |
||||
|
||||
export class EmojiArea { |
||||
private headerOffsets: number[]; |
||||
private currentCategory = 0; |
||||
private headers: HTMLElement[] = []; |
||||
private categoryButtons: CategoryButtons; |
||||
private emojisPerRow: number; |
||||
private categories: string[]; |
||||
|
||||
private focusedIndex = 0; |
||||
|
||||
container: HTMLElement; |
||||
emojis: HTMLElement; |
||||
|
||||
constructor( |
||||
private events: Emitter, |
||||
private i18n: I18NStrings, |
||||
private options: EmojiPickerOptions |
||||
) { |
||||
this.emojisPerRow = options.emojisPerRow || 8; |
||||
this.categories = options.categories || emojiData.categories; |
||||
|
||||
if (options.showRecents) { |
||||
this.categories = ['recents', ...this.categories]; |
||||
} |
||||
|
||||
if (options.custom) { |
||||
this.categories = [...this.categories, 'custom']; |
||||
} |
||||
} |
||||
|
||||
updateRecents(): void { |
||||
if (this.options.showRecents) { |
||||
emojiCategories.recents = load(); |
||||
const recentsContainer = this.emojis.querySelector( |
||||
`.${CLASS_EMOJI_CONTAINER}` |
||||
) as HTMLElement; |
||||
if (recentsContainer && recentsContainer.parentNode) { |
||||
recentsContainer.parentNode.replaceChild( |
||||
new EmojiContainer( |
||||
emojiCategories.recents, |
||||
true, |
||||
this.events, |
||||
this.options, |
||||
false |
||||
).render(), |
||||
recentsContainer |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
render(): HTMLElement { |
||||
this.container = createElement('div', CLASS_EMOJI_AREA); |
||||
|
||||
if (this.options.showCategoryButtons) { |
||||
this.categoryButtons = new CategoryButtons( |
||||
this.options, |
||||
this.events, |
||||
this.i18n |
||||
); |
||||
this.container.appendChild(this.categoryButtons.render()); |
||||
} |
||||
|
||||
this.emojis = createElement('div', CLASS_EMOJIS); |
||||
|
||||
if (this.options.showRecents) { |
||||
emojiCategories.recents = load(); |
||||
} |
||||
|
||||
if (this.options.custom) { |
||||
emojiCategories.custom = this.options.custom.map(custom => ({ |
||||
...custom, |
||||
custom: true |
||||
})); |
||||
} |
||||
|
||||
this.categories.forEach(category => |
||||
this.addCategory(category, emojiCategories[category]) |
||||
); |
||||
|
||||
requestAnimationFrame(() => { |
||||
setTimeout(() => { |
||||
setTimeout(() => |
||||
this.emojis.addEventListener('scroll', this.highlightCategory) |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
this.emojis.addEventListener('keydown', this.handleKeyDown); |
||||
|
||||
this.events.on(CATEGORY_CLICKED, this.selectCategory); |
||||
|
||||
this.container.appendChild(this.emojis); |
||||
|
||||
const firstEmoji = this.container.querySelectorAll( |
||||
`.${CLASS_EMOJI}` |
||||
)[0] as HTMLElement; |
||||
firstEmoji.tabIndex = 0; |
||||
|
||||
return this.container; |
||||
} |
||||
|
||||
reset(): void { |
||||
this.headerOffsets = Array.prototype.map.call( |
||||
this.headers, |
||||
header => header.offsetTop |
||||
) as number[]; |
||||
|
||||
this.selectCategory(this.options.initialCategory || 'smileys', false); |
||||
this.currentCategory = this.categories.indexOf( |
||||
(this.options.initialCategory as string) || 'smileys' |
||||
); |
||||
|
||||
if (this.options.showCategoryButtons) { |
||||
this.categoryButtons.setActiveButton(this.currentCategory, false); |
||||
} |
||||
} |
||||
|
||||
private get currentCategoryEl(): HTMLElement { |
||||
return this.emojis.querySelectorAll(`.${CLASS_EMOJI_CONTAINER}`)[ |
||||
this.currentCategory |
||||
] as HTMLElement; |
||||
} |
||||
|
||||
private get focusedEmoji(): HTMLElement { |
||||
return this.currentCategoryEl.querySelectorAll(`.${CLASS_EMOJI}`)[ |
||||
this.focusedIndex |
||||
] as HTMLElement; |
||||
} |
||||
|
||||
private get currentEmojiCount(): number { |
||||
return this.currentCategoryEl.querySelectorAll(`.${CLASS_EMOJI}`).length; |
||||
} |
||||
|
||||
private getEmojiCount(category: number): number { |
||||
const container = this.emojis.querySelectorAll(`.${CLASS_EMOJI_CONTAINER}`)[ |
||||
category |
||||
] as HTMLElement; |
||||
return container.querySelectorAll(`.${CLASS_EMOJI}`).length; |
||||
} |
||||
|
||||
private handleKeyDown = (event: KeyboardEvent): void => { |
||||
this.emojis.removeEventListener('scroll', this.highlightCategory); |
||||
switch (event.key) { |
||||
case 'ArrowRight': |
||||
this.focusedEmoji.tabIndex = -1; |
||||
|
||||
if ( |
||||
this.focusedIndex === this.currentEmojiCount - 1 && |
||||
this.currentCategory < this.categories.length - 1 |
||||
) { |
||||
if (this.options.showCategoryButtons) { |
||||
this.categoryButtons.setActiveButton(++this.currentCategory); |
||||
} |
||||
this.setFocusedEmoji(0); |
||||
} else if (this.focusedIndex < this.currentEmojiCount - 1) { |
||||
this.setFocusedEmoji(this.focusedIndex + 1); |
||||
} |
||||
break; |
||||
case 'ArrowLeft': |
||||
this.focusedEmoji.tabIndex = -1; |
||||
|
||||
if (this.focusedIndex === 0 && this.currentCategory > 0) { |
||||
if (this.options.showCategoryButtons) { |
||||
this.categoryButtons.setActiveButton(--this.currentCategory); |
||||
} |
||||
this.setFocusedEmoji(this.currentEmojiCount - 1); |
||||
} else { |
||||
this.setFocusedEmoji(Math.max(0, this.focusedIndex - 1)); |
||||
} |
||||
break; |
||||
case 'ArrowDown': |
||||
event.preventDefault(); |
||||
this.focusedEmoji.tabIndex = -1; |
||||
|
||||
if ( |
||||
this.focusedIndex + this.emojisPerRow >= this.currentEmojiCount && |
||||
this.currentCategory < this.categories.length - 1 |
||||
) { |
||||
this.currentCategory++; |
||||
if (this.options.showCategoryButtons) { |
||||
this.categoryButtons.setActiveButton(this.currentCategory); |
||||
} |
||||
this.setFocusedEmoji( |
||||
Math.min( |
||||
this.focusedIndex % this.emojisPerRow, |
||||
this.currentEmojiCount - 1 |
||||
) |
||||
); |
||||
} else if ( |
||||
this.currentEmojiCount - this.focusedIndex > |
||||
this.emojisPerRow |
||||
) { |
||||
this.setFocusedEmoji(this.focusedIndex + this.emojisPerRow); |
||||
} |
||||
break; |
||||
case 'ArrowUp': |
||||
event.preventDefault(); |
||||
this.focusedEmoji.tabIndex = -1; |
||||
|
||||
if (this.focusedIndex < this.emojisPerRow && this.currentCategory > 0) { |
||||
const previousCategoryCount = this.getEmojiCount( |
||||
this.currentCategory - 1 |
||||
); |
||||
let previousLastRowCount = previousCategoryCount % this.emojisPerRow; |
||||
if (previousLastRowCount === 0) { |
||||
previousLastRowCount = this.emojisPerRow; |
||||
} |
||||
const currentColumn = this.focusedIndex; |
||||
const newIndex = |
||||
currentColumn > previousLastRowCount - 1 |
||||
? previousCategoryCount - 1 |
||||
: previousCategoryCount - previousLastRowCount + currentColumn; |
||||
|
||||
this.currentCategory--; |
||||
if (this.options.showCategoryButtons) { |
||||
this.categoryButtons.setActiveButton(this.currentCategory); |
||||
} |
||||
|
||||
this.setFocusedEmoji(newIndex); |
||||
} else { |
||||
this.setFocusedEmoji( |
||||
this.focusedIndex >= this.emojisPerRow |
||||
? this.focusedIndex - this.emojisPerRow |
||||
: this.focusedIndex |
||||
); |
||||
} |
||||
break; |
||||
} |
||||
requestAnimationFrame(() => |
||||
this.emojis.addEventListener('scroll', this.highlightCategory) |
||||
); |
||||
}; |
||||
|
||||
private setFocusedEmoji(index: number, focus = true): void { |
||||
this.focusedIndex = index; |
||||
|
||||
if (this.focusedEmoji) { |
||||
this.focusedEmoji.tabIndex = 0; |
||||
|
||||
if (focus) { |
||||
this.focusedEmoji.focus(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private addCategory = ( |
||||
category: string, |
||||
emojis: Array<EmojiRecord | RecentEmoji> |
||||
): void => { |
||||
const name = createElement('h2', CLASS_CATEGORY_NAME); |
||||
name.innerHTML = |
||||
this.i18n.categories[category] || defaultI18n.categories[category]; |
||||
this.emojis.appendChild(name); |
||||
this.headers.push(name); |
||||
|
||||
this.emojis.appendChild( |
||||
new EmojiContainer( |
||||
emojis, |
||||
true, |
||||
this.events, |
||||
this.options, |
||||
category !== 'recents' |
||||
).render() |
||||
); |
||||
}; |
||||
|
||||
selectCategory = (category: string, focus = true): void => { |
||||
this.emojis.removeEventListener('scroll', this.highlightCategory); |
||||
if (this.focusedEmoji) { |
||||
this.focusedEmoji.tabIndex = -1; |
||||
} |
||||
|
||||
const categoryIndex = this.categories.indexOf(category); |
||||
this.currentCategory = categoryIndex; |
||||
this.setFocusedEmoji(0, false); |
||||
if (this.options.showCategoryButtons) { |
||||
this.categoryButtons.setActiveButton(this.currentCategory, focus); |
||||
} |
||||
|
||||
const targetPosition = this.headerOffsets[categoryIndex]; |
||||
this.emojis.scrollTop = targetPosition; |
||||
requestAnimationFrame(() => |
||||
this.emojis.addEventListener('scroll', this.highlightCategory) |
||||
); |
||||
}; |
||||
|
||||
highlightCategory = (): void => { |
||||
if ( |
||||
document.activeElement && |
||||
document.activeElement.classList.contains('emoji-picker__emoji') |
||||
) { |
||||
return; |
||||
} |
||||
|
||||
let closestHeaderIndex = this.headerOffsets.findIndex( |
||||
offset => offset >= Math.round(this.emojis.scrollTop) |
||||
); |
||||
|
||||
if ( |
||||
this.emojis.scrollTop + this.emojis.offsetHeight === |
||||
this.emojis.scrollHeight |
||||
) { |
||||
closestHeaderIndex = -1; |
||||
} |
||||
|
||||
if (closestHeaderIndex === 0) { |
||||
closestHeaderIndex = 1; |
||||
} else if (closestHeaderIndex < 0) { |
||||
closestHeaderIndex = this.headerOffsets.length; |
||||
} |
||||
|
||||
if (this.headerOffsets[closestHeaderIndex] === this.emojis.scrollTop) { |
||||
closestHeaderIndex++; |
||||
} |
||||
|
||||
this.currentCategory = closestHeaderIndex - 1; |
||||
if (this.options.showCategoryButtons) { |
||||
this.categoryButtons.setActiveButton(this.currentCategory); |
||||
} |
||||
}; |
||||
} |
@ -0,0 +1,45 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import { Emoji } from './emoji'; |
||||
import { createElement } from './util'; |
||||
|
||||
import { CLASS_EMOJI_CONTAINER } from './classes'; |
||||
|
||||
import { EmojiPickerOptions, EmojiRecord, RecentEmoji } from './types'; |
||||
|
||||
export class EmojiContainer { |
||||
private emojis: Array<EmojiRecord | RecentEmoji>; |
||||
|
||||
constructor( |
||||
emojis: Array<EmojiRecord | RecentEmoji>, |
||||
private showVariants: boolean, |
||||
private events: Emitter, |
||||
private options: EmojiPickerOptions, |
||||
private lazy = true |
||||
) { |
||||
this.emojis = emojis.filter( |
||||
e => |
||||
!(e as EmojiRecord).version || |
||||
parseFloat((e as EmojiRecord).version as string) <= |
||||
parseFloat(options.emojiVersion as string) |
||||
); |
||||
} |
||||
|
||||
render(): HTMLElement { |
||||
const emojiContainer = createElement('div', CLASS_EMOJI_CONTAINER); |
||||
this.emojis.forEach(emoji => |
||||
emojiContainer.appendChild( |
||||
new Emoji( |
||||
emoji, |
||||
this.showVariants, |
||||
true, |
||||
this.events, |
||||
this.options, |
||||
this.lazy |
||||
).render() |
||||
) |
||||
); |
||||
|
||||
return emojiContainer; |
||||
} |
||||
} |
@ -0,0 +1,8 @@
|
||||
export const EMOJI = 'emoji'; |
||||
export const SHOW_SEARCH_RESULTS = 'showSearchResults'; |
||||
export const HIDE_SEARCH_RESULTS = 'hideSearchResults'; |
||||
export const SHOW_PREVIEW = 'showPreview'; |
||||
export const HIDE_PREVIEW = 'hidePreview'; |
||||
export const HIDE_VARIANT_POPUP = 'hideVariantPopup'; |
||||
export const CATEGORY_CLICKED = 'categoryClicked'; |
||||
export const PICKER_HIDDEN = 'hidden'; |
@ -0,0 +1,19 @@
|
||||
import { I18NStrings } from './types'; |
||||
|
||||
export const i18n: I18NStrings = { |
||||
search: 'Search Emojis...', |
||||
categories: { |
||||
recents: 'Recent Emojis', |
||||
smileys: 'Smileys & Emotion', |
||||
people: 'People & Body', |
||||
animals: 'Animals & Nature', |
||||
food: 'Food & Drink', |
||||
activities: 'Activities', |
||||
travel: 'Travel & Places', |
||||
objects: 'Objects', |
||||
symbols: 'Symbols', |
||||
flags: 'Flags', |
||||
custom: 'Custom' |
||||
}, |
||||
notFound: 'No emojis found' |
||||
}; |
@ -0,0 +1,41 @@
|
||||
import { library, icon } from '@fortawesome/fontawesome-svg-core'; |
||||
import { faCat, faCoffee, faFutbol, faHistory, faIcons, faMusic, faSearch, faTimes, faUser } from '@fortawesome/free-solid-svg-icons'; |
||||
import { faBuilding, faFlag, faFrown, faLightbulb, faSmile } from '@fortawesome/free-regular-svg-icons'; |
||||
|
||||
library.add( |
||||
faBuilding, |
||||
faCat, |
||||
faCoffee, |
||||
faFlag, |
||||
faFrown, |
||||
faFutbol, |
||||
faHistory, |
||||
faIcons, |
||||
faLightbulb, |
||||
faMusic, |
||||
faSearch, |
||||
faSmile, |
||||
faTimes, |
||||
faUser |
||||
); |
||||
|
||||
export const building = icon({ prefix: 'far', iconName: 'building' }).html[0]; |
||||
export const cat = icon({ prefix: 'fas', iconName: 'cat' }).html[0]; |
||||
export const coffee = icon({ prefix: 'fas', iconName: 'coffee' }).html[0]; |
||||
export const flag = icon({ prefix: 'far', iconName: 'flag' }).html[0]; |
||||
export const futbol = icon({ prefix: 'fas', iconName: 'futbol' }).html[0]; |
||||
export const frown = icon({ prefix: 'far', iconName: 'frown' }).html[0]; |
||||
export const history = icon({ prefix: 'fas', iconName: 'history' }).html[0]; |
||||
export const icons = icon({ prefix: 'fas', iconName: 'icons' }).html[0]; |
||||
export const lightbulb = icon({ prefix: 'far', iconName: 'lightbulb' }).html[0]; |
||||
export const music = icon({ prefix: 'fas', iconName: 'music' }).html[0]; |
||||
export const search = icon({ prefix: 'fas', iconName: 'search' }).html[0]; |
||||
export const smile = icon({ prefix: 'far', iconName: 'smile' }).html[0]; |
||||
export const times = icon({ prefix: 'fas', iconName: 'times' }).html[0]; |
||||
export const user = icon({ prefix: 'fas', iconName: 'user' }).html[0]; |
||||
|
||||
export function createIcon(src: string): HTMLImageElement { |
||||
const img = document.createElement('img') as HTMLImageElement; |
||||
img.src = src; |
||||
return img; |
||||
}; |
@ -0,0 +1,579 @@
|
||||
import '../css/emoji-picker.css'; |
||||
|
||||
import createFocusTrap, { FocusTrap } from 'focus-trap'; |
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
import { createPopper, Instance as Popper, Placement } from '@popperjs/core'; |
||||
import twemoji from 'twemoji'; |
||||
|
||||
import emojiData from './data/emoji'; |
||||
|
||||
import { EMOJI, SHOW_SEARCH_RESULTS, HIDE_SEARCH_RESULTS, HIDE_VARIANT_POPUP, PICKER_HIDDEN } from './events'; |
||||
import { EmojiPreview } from './preview'; |
||||
import { Search } from './search'; |
||||
import { createElement, empty } from './util'; |
||||
import { VariantPopup } from './variantPopup'; |
||||
|
||||
import { i18n } from './i18n'; |
||||
|
||||
import { |
||||
CLASS_PICKER, CLASS_PICKER_CONTENT, CLASS_EMOJI, CLASS_SEARCH_FIELD, CLASS_VARIANT_OVERLAY, |
||||
CLASS_WRAPPER, CLASS_OVERLAY, CLASS_CUSTOM_EMOJI, CLASS_PLUGIN_CONTAINER |
||||
} from './classes'; |
||||
|
||||
import { EmojiPickerOptions, TwemojiOptions, I18NStrings, EmojiRecord, EmojiTheme } from './types'; |
||||
import { EmojiArea } from './emojiArea'; |
||||
|
||||
const DEFAULT_TWEMOJI_OPTIONS: TwemojiOptions = { |
||||
ext: '.svg', |
||||
folder: 'svg' |
||||
}; |
||||
|
||||
const DEFAULT_OPTIONS: EmojiPickerOptions = { |
||||
position: 'auto', |
||||
autoHide: true, |
||||
autoFocusSearch: true, |
||||
showAnimation: true, |
||||
showPreview: true, |
||||
showSearch: true, |
||||
showRecents: true, |
||||
showVariants: true, |
||||
showCategoryButtons: true, |
||||
recentsCount: 50, |
||||
emojiVersion: '12.1', |
||||
theme: 'light', |
||||
categories: [ |
||||
'smileys', |
||||
'people', |
||||
'animals', |
||||
'food', |
||||
'activities', |
||||
'travel', |
||||
'objects', |
||||
'symbols', |
||||
'flags' |
||||
], |
||||
style: 'native', |
||||
boxShadow: 'none', |
||||
emojisPerRow: 8, |
||||
rows: 6, |
||||
emojiSize: '1.8em', |
||||
initialCategory: 'smileys' |
||||
}; |
||||
|
||||
export class EmojiPicker { |
||||
private pickerVisible: boolean; |
||||
|
||||
private hideInProgress: boolean; |
||||
|
||||
private events = new Emitter(); |
||||
private publicEvents = new Emitter(); |
||||
private options: EmojiPickerOptions; |
||||
private twOptions: TwemojiOptions |
||||
private i18n: I18NStrings; |
||||
|
||||
private pickerEl: HTMLElement; |
||||
private pickerContent: HTMLElement; |
||||
private wrapper: HTMLElement; |
||||
private focusTrap: FocusTrap; |
||||
|
||||
private emojiArea: EmojiArea; |
||||
|
||||
private overlay?: HTMLElement; |
||||
|
||||
private popper: Popper; |
||||
|
||||
private observer: IntersectionObserver; |
||||
|
||||
private theme: EmojiTheme; |
||||
|
||||
constructor(options: EmojiPickerOptions = {}) { |
||||
this.pickerVisible = false; |
||||
|
||||
this.options = { ...DEFAULT_OPTIONS, ...options }; |
||||
|
||||
// Check for twemojiBaseUrl, if present add to the default options
|
||||
options.twemojiBaseUrl ? this.twOptions = { ...DEFAULT_TWEMOJI_OPTIONS, base: options.twemojiBaseUrl } : this.twOptions = { ...DEFAULT_TWEMOJI_OPTIONS }; |
||||
|
||||
if (!this.options.rootElement) { |
||||
this.options.rootElement = document.body; |
||||
} |
||||
|
||||
this.i18n = { |
||||
...i18n, |
||||
...options.i18n |
||||
}; |
||||
|
||||
this.onDocumentClick = this.onDocumentClick.bind(this); |
||||
this.onDocumentKeydown = this.onDocumentKeydown.bind(this); |
||||
this.hidePicker = this.hidePicker.bind(this); |
||||
|
||||
this.theme = this.options.theme || 'light'; |
||||
|
||||
this.initPicker(); |
||||
} |
||||
|
||||
private initPicker(): void { |
||||
this.pickerEl = createElement('div', CLASS_PICKER); |
||||
this.updateTheme(this.theme); |
||||
|
||||
if (!this.options.showAnimation) { |
||||
this.pickerEl.style.setProperty('--animation-duration', '0s'); |
||||
} |
||||
|
||||
this.options.emojisPerRow && |
||||
this.pickerEl.style.setProperty( |
||||
'--emoji-per-row', |
||||
this.options.emojisPerRow.toString() |
||||
); |
||||
this.options.rows && |
||||
this.pickerEl.style.setProperty( |
||||
'--row-count', |
||||
this.options.rows.toString() |
||||
); |
||||
this.options.emojiSize && |
||||
this.pickerEl.style.setProperty('--emoji-size', this.options.emojiSize); |
||||
|
||||
if (!this.options.showCategoryButtons) { |
||||
this.pickerEl.style.setProperty('--category-button-height', '0'); |
||||
} |
||||
|
||||
this.focusTrap = createFocusTrap(this.pickerEl as HTMLElement, { |
||||
clickOutsideDeactivates: true, |
||||
initialFocus: |
||||
this.options.showSearch && this.options.autoFocusSearch |
||||
? '.emoji-picker__search' |
||||
: '.emoji-picker__emoji[tabindex="0"]' |
||||
}); |
||||
|
||||
this.pickerContent = createElement('div', CLASS_PICKER_CONTENT); |
||||
|
||||
if (this.options.plugins) { |
||||
const pluginContainer = createElement('div', CLASS_PLUGIN_CONTAINER); |
||||
|
||||
this.options.plugins.forEach(plugin => { |
||||
if (!plugin.render) { |
||||
throw new Error( |
||||
'Emoji Button plugins must have a "render" function.' |
||||
); |
||||
} |
||||
pluginContainer.appendChild(plugin.render(this)); |
||||
}); |
||||
|
||||
this.pickerEl.appendChild(pluginContainer); |
||||
} |
||||
|
||||
if (this.options.showSearch) { |
||||
const searchContainer = new Search( |
||||
this.events, |
||||
this.i18n, |
||||
this.options, |
||||
emojiData.emoji, |
||||
(this.options.categories || []).map(category => |
||||
emojiData.categories.indexOf(category) |
||||
), |
||||
this.hidePicker |
||||
).render(); |
||||
this.pickerEl.appendChild(searchContainer); |
||||
} |
||||
|
||||
this.pickerEl.appendChild(this.pickerContent); |
||||
|
||||
this.emojiArea = new EmojiArea(this.events, this.i18n, this.options); |
||||
this.pickerContent.appendChild(this.emojiArea.render()); |
||||
|
||||
this.events.on(SHOW_SEARCH_RESULTS, (searchResults: HTMLElement) => { |
||||
empty(this.pickerContent); |
||||
searchResults.classList.add('search-results'); |
||||
this.pickerContent.appendChild(searchResults); |
||||
}); |
||||
|
||||
this.events.on(HIDE_SEARCH_RESULTS, () => { |
||||
if (this.pickerContent.firstChild !== this.emojiArea.container) { |
||||
empty(this.pickerContent); |
||||
this.pickerContent.appendChild(this.emojiArea.container); |
||||
} |
||||
|
||||
this.emojiArea.reset(); |
||||
}); |
||||
|
||||
if (this.options.showPreview) { |
||||
this.pickerEl.appendChild( |
||||
new EmojiPreview(this.events, this.options).render() |
||||
); |
||||
} |
||||
|
||||
let variantPopup: HTMLElement | null; |
||||
|
||||
this.events.on( |
||||
EMOJI, |
||||
({ |
||||
emoji, |
||||
showVariants |
||||
}: { |
||||
emoji: EmojiRecord; |
||||
showVariants: boolean; |
||||
}) => { |
||||
if ( |
||||
(emoji as EmojiRecord).variations && |
||||
showVariants && |
||||
this.options.showVariants |
||||
) { |
||||
this.showVariantPopup(emoji as EmojiRecord); |
||||
} else { |
||||
if (variantPopup && variantPopup.parentNode === this.pickerEl) { |
||||
this.events.emit(HIDE_VARIANT_POPUP); |
||||
} |
||||
|
||||
setTimeout(() => this.emojiArea.updateRecents()); |
||||
|
||||
if (emoji.custom) { |
||||
this.publicEvents.emit(EMOJI, { |
||||
url: emoji.emoji, |
||||
custom: true |
||||
}); |
||||
} else if (this.options.style === 'twemoji') { |
||||
twemoji.parse(emoji.emoji, { |
||||
...this.twOptions, |
||||
callback: (icon, options) => { |
||||
this.publicEvents.emit(EMOJI, { |
||||
url: `${options.base}${options.size}/${icon}${options.ext}`, |
||||
emoji: emoji.emoji |
||||
}); |
||||
} |
||||
}); |
||||
} else { |
||||
this.publicEvents.emit(EMOJI, { |
||||
emoji: emoji.emoji |
||||
}); |
||||
} |
||||
if (this.options.autoHide) { |
||||
this.hidePicker(); |
||||
} |
||||
} |
||||
} |
||||
); |
||||
|
||||
this.wrapper = createElement('div', CLASS_WRAPPER); |
||||
this.wrapper.appendChild(this.pickerEl); |
||||
this.wrapper.style.display = 'none'; |
||||
|
||||
if (this.options.zIndex) { |
||||
this.wrapper.style.zIndex = this.options.zIndex + ''; |
||||
} |
||||
|
||||
if (this.options.boxShadow) { |
||||
this.wrapper.style.boxShadow = this.options.boxShadow; |
||||
} |
||||
|
||||
|
||||
if (this.options.rootElement) { |
||||
this.options.rootElement.appendChild(this.wrapper); |
||||
} |
||||
|
||||
this.observeForLazyLoad(); |
||||
} |
||||
|
||||
on(event: string, callback: (arg: string) => void): void { |
||||
this.publicEvents.on(event, callback); |
||||
} |
||||
|
||||
off(event: string, callback: (arg: string) => void): void { |
||||
this.publicEvents.off(event, callback); |
||||
} |
||||
|
||||
getEmoji(emoji: String) { |
||||
|
||||
let response: Object = {}; |
||||
|
||||
twemoji.parse(emoji, { |
||||
...this.twOptions, |
||||
callback: (icon, options) => { |
||||
response = { |
||||
url: `${options.base}${options.size}/${icon}${options.ext}`, |
||||
emoji: emoji |
||||
} |
||||
return response; |
||||
} |
||||
}); |
||||
|
||||
return response; |
||||
} |
||||
|
||||
parse(htmlString: String) { |
||||
|
||||
return twemoji.parse(htmlString, { |
||||
...this.twOptions |
||||
}); |
||||
} |
||||
|
||||
private showVariantPopup(emoji: EmojiRecord) { |
||||
const variantPopup = new VariantPopup( |
||||
this.events, |
||||
emoji, |
||||
this.options |
||||
).render(); |
||||
|
||||
if (variantPopup) { |
||||
this.pickerEl.appendChild(variantPopup); |
||||
} |
||||
|
||||
this.events.on(HIDE_VARIANT_POPUP, () => { |
||||
if (variantPopup) { |
||||
variantPopup.classList.add('hiding'); |
||||
setTimeout(() => { |
||||
variantPopup && this.pickerEl.removeChild(variantPopup); |
||||
}, 175); |
||||
} |
||||
|
||||
this.events.off(HIDE_VARIANT_POPUP); |
||||
}); |
||||
} |
||||
|
||||
private observeForLazyLoad() { |
||||
const onChange = changes => { |
||||
const visibleElements = Array.prototype.filter |
||||
.call(changes, change => { |
||||
return change.intersectionRatio > 0; |
||||
}) |
||||
.map(entry => entry.target); |
||||
|
||||
visibleElements.forEach(element => { |
||||
if (!element.dataset.loaded) { |
||||
if (element.dataset.custom) { |
||||
const img = createElement( |
||||
'img', |
||||
CLASS_CUSTOM_EMOJI |
||||
) as HTMLImageElement; |
||||
img.src = element.dataset.emoji; |
||||
element.innerText = ''; |
||||
element.appendChild(img); |
||||
element.dataset.loaded = true; |
||||
element.style.opacity = 1; |
||||
} else if (this.options.style === 'twemoji') { |
||||
element.innerHTML = twemoji.parse( |
||||
element.dataset.emoji, |
||||
this.twOptions |
||||
); |
||||
element.dataset.loaded = true; |
||||
element.style.opacity = '1'; |
||||
} |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
this.observer = new IntersectionObserver(onChange, { |
||||
root: this.emojiArea.emojis |
||||
}); |
||||
|
||||
const emojiElements = this.emojiArea.emojis.querySelectorAll( |
||||
`.${CLASS_EMOJI}` |
||||
); |
||||
|
||||
emojiElements.forEach(element => { |
||||
if ( |
||||
this.options.style === 'twemoji' || |
||||
(element as HTMLElement).dataset.custom === 'true' |
||||
) { |
||||
this.observer.observe(element); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private onDocumentClick(event: MouseEvent): void { |
||||
if (!this.pickerEl.contains(event.target as Node)) { |
||||
this.hidePicker(); |
||||
} |
||||
} |
||||
|
||||
destroyPicker(): void { |
||||
this.events.off(EMOJI); |
||||
this.events.off(HIDE_VARIANT_POPUP); |
||||
|
||||
if (this.options.rootElement) { |
||||
this.options.rootElement.removeChild(this.wrapper); |
||||
|
||||
this.popper && this.popper.destroy(); |
||||
} |
||||
|
||||
this.observer && this.observer.disconnect(); |
||||
|
||||
if (this.options.plugins) { |
||||
this.options.plugins.forEach(plugin => { |
||||
plugin.destroy && plugin.destroy(); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
hidePicker(): void { |
||||
this.hideInProgress = true; |
||||
this.focusTrap.deactivate(); |
||||
this.pickerVisible = false; |
||||
this.popper && this.popper.destroy(); |
||||
|
||||
if (this.overlay) { |
||||
document.body.removeChild(this.overlay); |
||||
this.overlay = undefined; |
||||
} |
||||
|
||||
// In some browsers, the delayed hide was triggering the scroll event handler
|
||||
// and stealing the focus. Remove the scroll listener before doing the delayed hide.
|
||||
this.emojiArea.emojis.removeEventListener( |
||||
'scroll', |
||||
this.emojiArea.highlightCategory |
||||
); |
||||
|
||||
this.pickerEl.classList.add('hiding'); |
||||
setTimeout( |
||||
() => { |
||||
this.wrapper.style.display = 'none'; |
||||
this.pickerEl.classList.remove('hiding'); |
||||
|
||||
if (this.pickerContent.firstChild !== this.emojiArea.container) { |
||||
empty(this.pickerContent); |
||||
this.pickerContent.appendChild(this.emojiArea.container); |
||||
} |
||||
|
||||
const searchField = this.pickerEl.querySelector( |
||||
`.${CLASS_SEARCH_FIELD}` |
||||
) as HTMLInputElement; |
||||
if (searchField) { |
||||
searchField.value = ''; |
||||
} |
||||
|
||||
const variantOverlay = this.pickerEl.querySelector( |
||||
`.${CLASS_VARIANT_OVERLAY}` |
||||
); |
||||
if (variantOverlay) { |
||||
this.events.emit(HIDE_VARIANT_POPUP); |
||||
} |
||||
|
||||
this.hideInProgress = false; |
||||
|
||||
this.publicEvents.emit(PICKER_HIDDEN); |
||||
}, |
||||
this.options.showAnimation ? 170 : 0 |
||||
); |
||||
|
||||
setTimeout(() => { |
||||
document.removeEventListener('click', this.onDocumentClick); |
||||
document.removeEventListener('keydown', this.onDocumentKeydown); |
||||
}); |
||||
} |
||||
|
||||
showPicker(referenceEl: HTMLElement): void { |
||||
|
||||
if (this.hideInProgress) { |
||||
setTimeout(() => this.showPicker(referenceEl), 100); |
||||
return; |
||||
} |
||||
|
||||
this.pickerVisible = true; |
||||
this.wrapper.style.display = 'block'; |
||||
|
||||
if (window.matchMedia('screen and (max-width: 450px)').matches) { |
||||
const style = window.getComputedStyle(this.pickerEl); |
||||
const htmlEl = document.querySelector('html'); |
||||
const viewportHeight = htmlEl && htmlEl.clientHeight; |
||||
const viewportWidth = htmlEl && htmlEl.clientWidth; |
||||
|
||||
const height = parseInt(style.height); |
||||
const newTop = viewportHeight ? viewportHeight / 2 - height / 2 : 0; |
||||
|
||||
const width = parseInt(style.width); |
||||
const newLeft = viewportWidth ? viewportWidth / 2 - width / 2 : 0; |
||||
|
||||
this.wrapper.style.position = 'fixed'; |
||||
this.wrapper.style.top = `${newTop}px`; |
||||
this.wrapper.style.left = `${newLeft}px`; |
||||
this.wrapper.style.zIndex = '5000'; |
||||
|
||||
this.overlay = createElement('div', CLASS_OVERLAY); |
||||
document.body.appendChild(this.overlay); |
||||
} else if (typeof this.options.position === 'string') { |
||||
this.popper = createPopper(referenceEl, this.wrapper, { |
||||
placement: this.options.position as Placement, |
||||
modifiers: [ |
||||
{ |
||||
name: 'offset', |
||||
options: { |
||||
offset: [0, 15] |
||||
} |
||||
} |
||||
] |
||||
}); |
||||
} else if ( |
||||
this.options.position && |
||||
(this.options.position.top || this.options.position.left) |
||||
) { |
||||
this.wrapper.style.position = 'fixed'; |
||||
|
||||
|
||||
|
||||
if (this.options.position.top) { |
||||
this.wrapper.style.top = this.options.position.top; |
||||
} |
||||
|
||||
if (this.options.position.bottom) { |
||||
this.wrapper.style.bottom = this.options.position.bottom; |
||||
} |
||||
|
||||
if (this.options.position.left) { |
||||
this.wrapper.style.left = this.options.position.left; |
||||
} |
||||
|
||||
if (this.options.position.right) { |
||||
this.wrapper.style.right = this.options.position.right; |
||||
} |
||||
} |
||||
|
||||
this.focusTrap.activate(); |
||||
|
||||
setTimeout(() => { |
||||
document.addEventListener('click', this.onDocumentClick); |
||||
document.addEventListener('keydown', this.onDocumentKeydown); |
||||
|
||||
const initialFocusElement = this.pickerEl.querySelector( |
||||
this.options.showSearch && this.options.autoFocusSearch |
||||
? `.${CLASS_SEARCH_FIELD}` |
||||
: `.${CLASS_EMOJI}[tabindex="0"]` |
||||
) as HTMLElement; |
||||
initialFocusElement.focus(); |
||||
}); |
||||
|
||||
this.emojiArea.reset(); |
||||
} |
||||
|
||||
togglePicker(referenceEl: HTMLElement): void { |
||||
|
||||
this.pickerVisible ? this.hidePicker() : this.showPicker(referenceEl); |
||||
} |
||||
|
||||
isPickerVisible(): boolean { |
||||
return this.pickerVisible; |
||||
} |
||||
|
||||
private onDocumentKeydown(event: KeyboardEvent): void { |
||||
if (event.key === 'Escape') { |
||||
this.hidePicker(); |
||||
} else if (event.key === 'Tab') { |
||||
this.pickerEl.classList.add('keyboard'); |
||||
} else if (event.key.match(/^[\w]$/)) { |
||||
const searchField = this.pickerEl.querySelector( |
||||
`.${CLASS_SEARCH_FIELD}` |
||||
) as HTMLInputElement; |
||||
searchField && searchField.focus(); |
||||
} |
||||
} |
||||
|
||||
setTheme(theme: EmojiTheme): void { |
||||
if (theme === this.theme) return; |
||||
|
||||
this.pickerEl.classList.remove(this.theme); |
||||
this.theme = theme; |
||||
this.updateTheme(this.theme); |
||||
} |
||||
|
||||
private updateTheme(theme: EmojiTheme): void { |
||||
this.pickerEl.classList.add(theme); |
||||
} |
||||
} |
@ -0,0 +1,66 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import twemoji from 'twemoji'; |
||||
|
||||
import { SHOW_PREVIEW, HIDE_PREVIEW } from './events'; |
||||
import { createElement } from './util'; |
||||
import { EmojiRecord, EmojiPickerOptions, TwemojiOptions } from './types'; |
||||
|
||||
import { CLASS_PREVIEW, CLASS_PREVIEW_EMOJI, CLASS_PREVIEW_NAME, CLASS_CUSTOM_EMOJI } from './classes'; |
||||
|
||||
const DEFAULT_TWEMOJI_OPTIONS: TwemojiOptions = { |
||||
ext: '.svg', |
||||
folder: 'svg' |
||||
}; |
||||
|
||||
export class EmojiPreview { |
||||
private emoji: HTMLElement; |
||||
private name: HTMLElement; |
||||
|
||||
private options: EmojiPickerOptions; |
||||
private twOptions: TwemojiOptions; |
||||
|
||||
constructor(private events: Emitter, options: EmojiPickerOptions) { |
||||
|
||||
this.options = options |
||||
|
||||
// Check for twemojiBaseUrl, if present add to the default options
|
||||
options.twemojiBaseUrl ? this.twOptions = { ...DEFAULT_TWEMOJI_OPTIONS, base: options.twemojiBaseUrl } : this.twOptions = { ...DEFAULT_TWEMOJI_OPTIONS } |
||||
|
||||
} |
||||
|
||||
render(): HTMLElement { |
||||
const preview = createElement('div', CLASS_PREVIEW); |
||||
|
||||
this.emoji = createElement('div', CLASS_PREVIEW_EMOJI); |
||||
preview.appendChild(this.emoji); |
||||
|
||||
this.name = createElement('div', CLASS_PREVIEW_NAME); |
||||
preview.appendChild(this.name); |
||||
|
||||
this.events.on(SHOW_PREVIEW, (emoji: EmojiRecord) => |
||||
this.showPreview(emoji) |
||||
); |
||||
this.events.on(HIDE_PREVIEW, () => this.hidePreview()); |
||||
|
||||
return preview; |
||||
} |
||||
|
||||
showPreview(emoji: EmojiRecord): void { |
||||
let content = emoji.emoji; |
||||
|
||||
if (emoji.custom) { |
||||
content = `<img class="${CLASS_CUSTOM_EMOJI}" src="${emoji.emoji}">`; |
||||
} else if (this.options.style === 'twemoji') { |
||||
content = twemoji.parse(emoji.emoji, this.twOptions); |
||||
} |
||||
|
||||
this.emoji.innerHTML = content; |
||||
this.name.innerHTML = emoji.name; |
||||
} |
||||
|
||||
hidePreview(): void { |
||||
this.emoji.innerHTML = ''; |
||||
this.name.innerHTML = ''; |
||||
} |
||||
} |
@ -0,0 +1,33 @@
|
||||
import { EmojiRecord, EmojiPickerOptions, RecentEmoji } from './types'; |
||||
|
||||
const LOCAL_STORAGE_KEY = 'emojiPicker.recent'; |
||||
|
||||
export function load(): Array<RecentEmoji> { |
||||
const recentJson = localStorage.getItem(LOCAL_STORAGE_KEY); |
||||
const recents = recentJson ? JSON.parse(recentJson) : []; |
||||
return recents.filter(recent => !!recent.emoji); |
||||
} |
||||
|
||||
export function save( |
||||
emoji: EmojiRecord | RecentEmoji, |
||||
options: EmojiPickerOptions |
||||
): void { |
||||
const recents = load(); |
||||
|
||||
const recent = { |
||||
emoji: emoji.emoji, |
||||
name: emoji.name, |
||||
key: (emoji as RecentEmoji).key || emoji.name, |
||||
custom: emoji.custom |
||||
}; |
||||
|
||||
localStorage.setItem( |
||||
LOCAL_STORAGE_KEY, |
||||
JSON.stringify( |
||||
[ |
||||
recent, |
||||
...recents.filter((r: RecentEmoji) => !!r.emoji && r.key !== recent.key) |
||||
].slice(0, options.recentsCount) |
||||
) |
||||
); |
||||
} |
@ -0,0 +1,253 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import * as icons from './icons'; |
||||
|
||||
import { EmojiContainer } from './emojiContainer'; |
||||
import { HIDE_PREVIEW, HIDE_VARIANT_POPUP, SHOW_SEARCH_RESULTS, HIDE_SEARCH_RESULTS } from './events'; |
||||
import { createElement, empty } from './util'; |
||||
import { I18NStrings, EmojiPickerOptions, EmojiRecord } from './types'; |
||||
|
||||
import { CLASS_SEARCH_CONTAINER, CLASS_SEARCH_FIELD, CLASS_SEARCH_ICON, CLASS_NOT_FOUND, CLASS_NOT_FOUND_ICON, CLASS_EMOJI } from './classes'; |
||||
|
||||
import fuzzysort from 'fuzzysort'; |
||||
|
||||
class NotFoundMessage { |
||||
constructor(private message: string, private iconUrl?: string) { } |
||||
|
||||
render(): HTMLElement { |
||||
const container = createElement('div', CLASS_NOT_FOUND); |
||||
|
||||
const iconContainer = createElement('div', CLASS_NOT_FOUND_ICON); |
||||
|
||||
if (this.iconUrl) { |
||||
iconContainer.appendChild(icons.createIcon(this.iconUrl)); |
||||
} else { |
||||
iconContainer.innerHTML = icons.frown; |
||||
} |
||||
|
||||
container.appendChild(iconContainer); |
||||
|
||||
const messageContainer = createElement('h2'); |
||||
messageContainer.innerHTML = this.message; |
||||
container.appendChild(messageContainer); |
||||
|
||||
return container; |
||||
} |
||||
} |
||||
|
||||
export class Search { |
||||
private emojiData: EmojiRecord[]; |
||||
private emojisPerRow: number; |
||||
private focusedEmojiIndex = 0; |
||||
|
||||
private searchContainer: HTMLElement; |
||||
private searchField: HTMLInputElement; |
||||
private searchIcon: HTMLElement; |
||||
private resultsContainer: HTMLElement | null; |
||||
|
||||
constructor( |
||||
private events: Emitter, |
||||
private i18n: I18NStrings, |
||||
private options: EmojiPickerOptions, |
||||
emojiData: EmojiRecord[], |
||||
categories: number[], |
||||
private hidePicker: Function |
||||
) { |
||||
this.emojisPerRow = this.options.emojisPerRow || 8; |
||||
this.emojiData = emojiData.filter( |
||||
e => |
||||
e.version && |
||||
parseFloat(e.version) <= parseFloat(options.emojiVersion as string) && |
||||
e.category !== undefined && |
||||
categories.indexOf(e.category) >= 0 |
||||
); |
||||
|
||||
if (this.options.custom) { |
||||
const customEmojis = this.options.custom.map(custom => ({ |
||||
...custom, |
||||
custom: true |
||||
})); |
||||
|
||||
this.emojiData = [...this.emojiData, ...customEmojis]; |
||||
} |
||||
|
||||
this.events.on(HIDE_VARIANT_POPUP, () => { |
||||
setTimeout(() => this.setFocusedEmoji(this.focusedEmojiIndex)); |
||||
}); |
||||
} |
||||
|
||||
render(): HTMLElement { |
||||
this.searchContainer = createElement('div', CLASS_SEARCH_CONTAINER); |
||||
|
||||
this.searchField = createElement( |
||||
'input', |
||||
CLASS_SEARCH_FIELD |
||||
) as HTMLInputElement; |
||||
this.searchField.placeholder = this.i18n.search; |
||||
this.searchContainer.appendChild(this.searchField); |
||||
|
||||
this.searchIcon = createElement('span', CLASS_SEARCH_ICON); |
||||
|
||||
if (this.options.icons && this.options.icons.search) { |
||||
this.searchIcon.appendChild(icons.createIcon(this.options.icons.search)); |
||||
} else { |
||||
this.searchIcon.innerHTML = icons.search; |
||||
} |
||||
|
||||
this.searchIcon.addEventListener('click', (event: MouseEvent) => |
||||
this.onClearSearch(event) |
||||
); |
||||
|
||||
this.searchContainer.appendChild(this.searchIcon); |
||||
|
||||
this.searchField.addEventListener('keydown', (event: KeyboardEvent) => { |
||||
this.onKeyDown(event); |
||||
|
||||
event.stopPropagation(); |
||||
return false; |
||||
}); |
||||
|
||||
this.searchField.addEventListener('keyup', (event: KeyboardEvent) => this.onKeyUp(event)); |
||||
|
||||
return this.searchContainer; |
||||
} |
||||
|
||||
onClearSearch(event: Event): void { |
||||
event.stopPropagation(); |
||||
|
||||
if (this.searchField.value) { |
||||
this.searchField.value = ''; |
||||
this.resultsContainer = null; |
||||
|
||||
if (this.options.icons && this.options.icons.search) { |
||||
empty(this.searchIcon); |
||||
this.searchIcon.appendChild( |
||||
icons.createIcon(this.options.icons.search) |
||||
); |
||||
} else { |
||||
this.searchIcon.innerHTML = icons.search; |
||||
} |
||||
|
||||
this.searchIcon.style.cursor = 'default'; |
||||
|
||||
this.events.emit(HIDE_SEARCH_RESULTS); |
||||
|
||||
setTimeout(() => this.searchField.focus()); |
||||
} |
||||
} |
||||
|
||||
setFocusedEmoji(index: number): void { |
||||
if (this.resultsContainer) { |
||||
const emojis = this.resultsContainer.querySelectorAll(`.${CLASS_EMOJI}`); |
||||
const currentFocusedEmoji = emojis[this.focusedEmojiIndex] as HTMLElement; |
||||
currentFocusedEmoji.tabIndex = -1; |
||||
|
||||
this.focusedEmojiIndex = index; |
||||
const newFocusedEmoji = emojis[this.focusedEmojiIndex] as HTMLElement; |
||||
newFocusedEmoji.tabIndex = 0; |
||||
newFocusedEmoji.focus(); |
||||
} |
||||
} |
||||
|
||||
handleResultsKeydown(event: KeyboardEvent): void { |
||||
if (this.resultsContainer) { |
||||
const emojis = this.resultsContainer.querySelectorAll(`.${CLASS_EMOJI}`); |
||||
if (event.key === 'ArrowRight') { |
||||
this.setFocusedEmoji( |
||||
Math.min(this.focusedEmojiIndex + 1, emojis.length - 1) |
||||
); |
||||
} else if (event.key === 'ArrowLeft') { |
||||
this.setFocusedEmoji(Math.max(0, this.focusedEmojiIndex - 1)); |
||||
} else if (event.key === 'ArrowDown') { |
||||
event.preventDefault(); |
||||
if (this.focusedEmojiIndex < emojis.length - this.emojisPerRow) { |
||||
this.setFocusedEmoji(this.focusedEmojiIndex + this.emojisPerRow); |
||||
} |
||||
} else if (event.key === 'ArrowUp') { |
||||
event.preventDefault(); |
||||
if (this.focusedEmojiIndex >= this.emojisPerRow) { |
||||
this.setFocusedEmoji(this.focusedEmojiIndex - this.emojisPerRow); |
||||
} |
||||
} else if (event.key === 'Escape') { |
||||
this.onClearSearch(event); |
||||
} |
||||
} |
||||
} |
||||
|
||||
onKeyDown(event: KeyboardEvent): void { |
||||
if (event.key === 'Escape' && this.searchField.value) { |
||||
this.onClearSearch(event); |
||||
} else if (event.key === 'Escape' && !this.searchField.value) { |
||||
this.hidePicker(); |
||||
} |
||||
} |
||||
|
||||
onKeyUp(event: KeyboardEvent): void { |
||||
if (event.key === 'Tab' || event.key === 'Shift') { |
||||
return; |
||||
} else if (!this.searchField.value) { |
||||
if (this.options.icons && this.options.icons.search) { |
||||
empty(this.searchIcon); |
||||
this.searchIcon.appendChild( |
||||
icons.createIcon(this.options.icons.search) |
||||
); |
||||
} else { |
||||
this.searchIcon.innerHTML = icons.search; |
||||
} |
||||
|
||||
this.searchIcon.style.cursor = 'default'; |
||||
this.events.emit(HIDE_SEARCH_RESULTS); |
||||
} else { |
||||
if (this.options.icons && this.options.icons.clearSearch) { |
||||
empty(this.searchIcon); |
||||
this.searchIcon.appendChild( |
||||
icons.createIcon(this.options.icons.clearSearch) |
||||
); |
||||
} else { |
||||
this.searchIcon.innerHTML = icons.times; |
||||
} |
||||
this.searchIcon.style.cursor = 'pointer'; |
||||
|
||||
const searchResults = fuzzysort |
||||
.go(this.searchField.value, this.emojiData, { |
||||
allowTypo: true, |
||||
limit: 100, |
||||
key: 'name' |
||||
}) |
||||
.map(result => result.obj); |
||||
|
||||
this.events.emit(HIDE_PREVIEW); |
||||
|
||||
if (searchResults.length) { |
||||
this.resultsContainer = new EmojiContainer( |
||||
searchResults, |
||||
true, |
||||
this.events, |
||||
this.options, |
||||
false |
||||
).render(); |
||||
|
||||
if (this.resultsContainer) { |
||||
(this.resultsContainer.querySelector( |
||||
`.${CLASS_EMOJI}` |
||||
) as HTMLElement).tabIndex = 0; |
||||
this.focusedEmojiIndex = 0; |
||||
|
||||
this.resultsContainer.addEventListener('keydown', event => |
||||
this.handleResultsKeydown(event) |
||||
); |
||||
|
||||
this.events.emit(SHOW_SEARCH_RESULTS, this.resultsContainer); |
||||
} |
||||
} else { |
||||
this.events.emit( |
||||
SHOW_SEARCH_RESULTS, |
||||
new NotFoundMessage( |
||||
this.i18n.notFound, |
||||
this.options.icons && this.options.icons.notFound |
||||
).render() |
||||
); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,132 @@
|
||||
import { Placement } from '@popperjs/core'; |
||||
import { EmojiPicker } from './index'; |
||||
|
||||
export interface EmojiRecord { |
||||
name: string; |
||||
emoji: string; |
||||
custom?: boolean; |
||||
category?: number; |
||||
version?: string; |
||||
variations?: string[]; |
||||
key?: string; |
||||
} |
||||
|
||||
export interface EmojiData { |
||||
categories: string[]; |
||||
emojiData: EmojiRecord[]; |
||||
} |
||||
|
||||
export interface RecentEmoji { |
||||
key: string; |
||||
name: string; |
||||
emoji: string; |
||||
custom?: boolean; |
||||
} |
||||
|
||||
export interface EmojiEventData { |
||||
emoji: EmojiRecord; |
||||
showVariants: boolean; |
||||
button: HTMLElement; |
||||
} |
||||
|
||||
export interface Plugin { |
||||
render(picker: EmojiPicker): HTMLElement; |
||||
destroy?(): void; |
||||
} |
||||
|
||||
export interface EmojiPickerOptions { |
||||
position?: Placement | FixedPosition; |
||||
autoHide?: boolean; |
||||
autoFocusSearch?: boolean; |
||||
showAnimation?: boolean; |
||||
showPreview?: boolean; |
||||
showSearch?: boolean; |
||||
showRecents?: boolean; |
||||
showVariants?: boolean; |
||||
showCategoryButtons?: boolean; |
||||
recentsCount?: number; |
||||
rootElement?: HTMLElement; |
||||
emojiVersion?: EmojiVersion; |
||||
i18n?: I18NStrings; |
||||
zIndex?: number; |
||||
boxShadow?: string | 'none'; |
||||
theme?: EmojiTheme; |
||||
categories?: Category[]; |
||||
style?: EmojiStyle; |
||||
twemojiBaseUrl?: string; |
||||
emojisPerRow?: number; |
||||
rows?: number; |
||||
emojiSize?: string; |
||||
initialCategory?: Category | 'recents'; |
||||
custom?: EmojiRecord[]; |
||||
plugins?: Plugin[]; |
||||
icons?: Icons; |
||||
} |
||||
|
||||
export interface TwemojiOptions { |
||||
base?: string, |
||||
ext: string, |
||||
folder: string |
||||
} |
||||
|
||||
export interface FixedPosition { |
||||
top?: string; |
||||
bottom?: string; |
||||
left?: string; |
||||
right?: string; |
||||
} |
||||
|
||||
export type EmojiStyle = 'native' | 'twemoji'; |
||||
|
||||
export type EmojiTheme = 'dark' | 'light' | 'auto'; |
||||
|
||||
export type EmojiVersion = |
||||
| '1.0' |
||||
| '2.0' |
||||
| '3.0' |
||||
| '4.0' |
||||
| '5.0' |
||||
| '11.0' |
||||
| '12.0' |
||||
| '12.1'; |
||||
|
||||
export type Category = |
||||
| 'smileys' |
||||
| 'people' |
||||
| 'animals' |
||||
| 'food' |
||||
| 'activities' |
||||
| 'travel' |
||||
| 'objects' |
||||
| 'symbols' |
||||
| 'flags'; |
||||
|
||||
export type I18NCategory = |
||||
| 'recents' |
||||
| 'smileys' |
||||
| 'people' |
||||
| 'animals' |
||||
| 'food' |
||||
| 'activities' |
||||
| 'travel' |
||||
| 'objects' |
||||
| 'symbols' |
||||
| 'flags' |
||||
| 'custom'; |
||||
|
||||
export interface I18NStrings { |
||||
search: string; |
||||
categories: { |
||||
[key in I18NCategory]: string; |
||||
}; |
||||
notFound: string; |
||||
} |
||||
|
||||
export interface Icons { |
||||
search?: string; |
||||
clearSearch?: string; |
||||
categories?: { |
||||
[key in I18NCategory]?: string; |
||||
}; |
||||
notFound?: string; |
||||
} |
@ -0,0 +1,17 @@
|
||||
import * as util from './util'; |
||||
|
||||
describe('Utils', () => { |
||||
describe('formatEmojiName', () => { |
||||
test('should format a dash-separated name', () => { |
||||
expect(util.formatEmojiName('foo-bar-baz')).toEqual('Foo bar baz'); |
||||
}); |
||||
|
||||
test('should format an underscore-separated name', () => { |
||||
expect(util.formatEmojiName('foo_bar_baz')).toEqual('Foo bar baz'); |
||||
}); |
||||
|
||||
test('should format a name separated by dashes and underscores', () => { |
||||
expect(util.formatEmojiName('foo_bar-baz')).toEqual('Foo bar baz'); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,25 @@
|
||||
export function createElement( |
||||
tagName: string, |
||||
className?: string |
||||
): HTMLElement { |
||||
const element = document.createElement(tagName); |
||||
|
||||
if (className) { |
||||
element.className = className; |
||||
} |
||||
|
||||
return element; |
||||
} |
||||
|
||||
export function empty(element: HTMLElement): void { |
||||
while (element.firstChild) { |
||||
element.removeChild(element.firstChild); |
||||
} |
||||
} |
||||
|
||||
export function formatEmojiName(name: string): string { |
||||
const words = name.split(/[-_]/); |
||||
words[0] = words[0][0].toUpperCase() + words[0].slice(1); |
||||
|
||||
return words.join(' '); |
||||
} |
@ -0,0 +1,29 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import { VariantPopup } from './variantPopup'; |
||||
|
||||
describe('VariantPopup', () => { |
||||
const emoji = { |
||||
name: 'thumbs up', |
||||
category: 0, |
||||
emoji: '👍', |
||||
variations: ['👍🏻', '👍🏿'], |
||||
version: '11.0' |
||||
}; |
||||
|
||||
let events; |
||||
let container; |
||||
|
||||
beforeEach(() => { |
||||
events = new Emitter(); |
||||
container = new VariantPopup(events, emoji, { style: 'native' }).render(); |
||||
}); |
||||
|
||||
test('should render the emoji variants', () => { |
||||
const EmojiPickers = container.querySelectorAll('.emoji-picker__emoji'); |
||||
|
||||
expect(EmojiPickers[0].innerHTML).toEqual(emoji.emoji); |
||||
expect(EmojiPickers[1].innerHTML).toEqual(emoji.variations[0]); |
||||
expect(EmojiPickers[2].innerHTML).toEqual(emoji.variations[1]); |
||||
}); |
||||
}); |
@ -0,0 +1,108 @@
|
||||
import { TinyEmitter as Emitter } from 'tiny-emitter'; |
||||
|
||||
import { Emoji } from './emoji'; |
||||
import { createElement } from './util'; |
||||
|
||||
import { HIDE_VARIANT_POPUP } from './events'; |
||||
|
||||
import { EmojiRecord, EmojiPickerOptions } from './types'; |
||||
|
||||
import { CLASS_VARIANT_OVERLAY, CLASS_VARIANT_POPUP, CLASS_EMOJI } from './classes'; |
||||
|
||||
export class VariantPopup { |
||||
private popup: HTMLElement; |
||||
private focusedEmojiIndex = 0; |
||||
|
||||
constructor( |
||||
private events: Emitter, |
||||
private emoji: EmojiRecord, |
||||
private options: EmojiPickerOptions |
||||
) { } |
||||
|
||||
getEmoji(index: number): Element { |
||||
return this.popup.querySelectorAll(`.${CLASS_EMOJI}`)[index]; |
||||
} |
||||
|
||||
setFocusedEmoji(newIndex: number): void { |
||||
const currentFocusedEmoji = this.getEmoji( |
||||
this.focusedEmojiIndex |
||||
) as HTMLElement; |
||||
currentFocusedEmoji.tabIndex = -1; |
||||
|
||||
this.focusedEmojiIndex = newIndex; |
||||
const newFocusedEmoji = this.getEmoji( |
||||
this.focusedEmojiIndex |
||||
) as HTMLElement; |
||||
newFocusedEmoji.tabIndex = 0; |
||||
newFocusedEmoji.focus(); |
||||
} |
||||
|
||||
render(): HTMLElement { |
||||
this.popup = createElement('div', CLASS_VARIANT_POPUP); |
||||
|
||||
const overlay = createElement('div', CLASS_VARIANT_OVERLAY); |
||||
overlay.addEventListener('click', (event: MouseEvent) => { |
||||
event.stopPropagation(); |
||||
|
||||
if (!this.popup.contains(event.target as Node)) { |
||||
this.events.emit(HIDE_VARIANT_POPUP); |
||||
} |
||||
}); |
||||
|
||||
this.popup.appendChild( |
||||
new Emoji( |
||||
this.emoji, |
||||
false, |
||||
false, |
||||
this.events, |
||||
this.options, |
||||
false |
||||
).render() |
||||
); |
||||
|
||||
(this.emoji.variations || []).forEach((variation, index) => |
||||
this.popup.appendChild( |
||||
new Emoji( |
||||
{ |
||||
name: this.emoji.name, |
||||
emoji: variation, |
||||
key: this.emoji.name + index |
||||
}, |
||||
false, |
||||
false, |
||||
this.events, |
||||
this.options, |
||||
false |
||||
).render() |
||||
) |
||||
); |
||||
|
||||
const firstEmoji = this.popup.querySelector( |
||||
`.${CLASS_EMOJI}` |
||||
) as HTMLElement; |
||||
this.focusedEmojiIndex = 0; |
||||
firstEmoji.tabIndex = 0; |
||||
|
||||
setTimeout(() => firstEmoji.focus()); |
||||
|
||||
this.popup.addEventListener('keydown', event => { |
||||
if (event.key === 'ArrowRight') { |
||||
this.setFocusedEmoji( |
||||
Math.min( |
||||
this.focusedEmojiIndex + 1, |
||||
this.popup.querySelectorAll(`.${CLASS_EMOJI}`).length - 1 |
||||
) |
||||
); |
||||
} else if (event.key === 'ArrowLeft') { |
||||
this.setFocusedEmoji(Math.max(this.focusedEmojiIndex - 1, 0)); |
||||
} else if (event.key === 'Escape') { |
||||
event.stopPropagation(); |
||||
this.events.emit(HIDE_VARIANT_POPUP); |
||||
} |
||||
}); |
||||
|
||||
overlay.appendChild(this.popup); |
||||
|
||||
return overlay; |
||||
} |
||||
} |
@ -0,0 +1,67 @@
|
||||
{ |
||||
"compilerOptions": { |
||||
/* Basic Options */ |
||||
// "incremental": true, /* Enable incremental compilation */ |
||||
"target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ |
||||
"module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ |
||||
// "lib": [], /* Specify library files to be included in the compilation. */ |
||||
// "allowJs": true, /* Allow javascript files to be compiled. */ |
||||
// "checkJs": true, /* Report errors in .js files. */ |
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ |
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */ |
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ |
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */ |
||||
// "outFile": "./", /* Concatenate and emit output to single file. */ |
||||
// "outDir": "./", /* Redirect output structure to the directory. */ |
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ |
||||
// "composite": true, /* Enable project compilation */ |
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ |
||||
// "removeComments": true, /* Do not emit comments to output. */ |
||||
// "noEmit": true, /* Do not emit outputs. */ |
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ |
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ |
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ |
||||
|
||||
/* Strict Type-Checking Options */ |
||||
"strict": true, /* Enable all strict type-checking options. */ |
||||
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ |
||||
// "strictNullChecks": true, /* Enable strict null checks. */ |
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ |
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ |
||||
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ |
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ |
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ |
||||
|
||||
/* Additional Checks */ |
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */ |
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */ |
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ |
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ |
||||
|
||||
/* Module Resolution Options */ |
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ |
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ |
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ |
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ |
||||
// "typeRoots": [], /* List of folders to include type definitions from. */ |
||||
// "types": [], /* Type declaration files to be included in compilation. */ |
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ |
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ |
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ |
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ |
||||
|
||||
/* Source Map Options */ |
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ |
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ |
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ |
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ |
||||
|
||||
/* Experimental Options */ |
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ |
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ |
||||
|
||||
/* Advanced Options */ |
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ |
||||
}, |
||||
"include": ["src/*.ts"] |
||||
} |
Loading…
Reference in new issue