Browse Source

Implement custom emojis

master
Joe Attardi 4 years ago
parent
commit
0712721272
  1. 14
      README.md
  2. 8
      css/emoji-button.css
  3. 4
      index.d.ts
  4. 9
      src/categoryButtons.ts
  5. 1
      src/classes.ts
  6. 38
      src/emoji.ts
  7. 38
      src/emojiArea.ts
  8. 3
      src/i18n.ts
  9. 3
      src/icons.ts
  10. 75
      src/index.ts
  11. 16
      src/preview.ts
  12. 3
      src/recent.ts
  13. 6
      src/types.ts

14
README.md

@ -51,6 +51,15 @@ The picker is shown by calling `showPicker` or `togglePicker` on the `EmojiButto
});
```
## Custom emojis
Emoji Button supports a custom emojis category. A custom emoji is an image file that is added to the picker. For best results, a custom emoji should be a square image.
A custom emoji is defined as an object with the following properties:
* `name`: The display name of the custom emoji.
* `emoji`: The URL of the image to use for the custom emoji.
### TypeScript note
Because the `EmojiButton` class is a default export, it requires a small tweak to import the library in a TypeScript project. There are two options:
@ -83,6 +92,8 @@ Creates an Emoji Button emoji picker.
* `symbols`
* `flags`
* `custom`: (object[]): An array of custom emojis to include in the "Custom" category.
* `emojiSize`: (string, default: `1.8em`) The size to use for the emoji icons.
* `emojisPerRow`: (number, default: `8`) The number of emojis to display per row. If this is set to a number smaller than 6, some category buttons may be cut off, so it is advisable to set `showCategoryButtons` to `false`.
@ -158,7 +169,8 @@ Creates an Emoji Button emoji picker.
travel: 'Travel & Places',
objects: 'Objects',
symbols: 'Symbols',
flags: 'Flags'
flags: 'Flags',
custom: 'Custom'
},
notFound: 'No emojis found'
}

8
css/emoji-button.css

@ -195,6 +195,11 @@
overflow-y: auto;
}
.emoji-picker__custom-emoji {
width: 1em;
height: 1em;
}
.emoji-picker__emoji {
background: transparent;
border: none;
@ -207,6 +212,9 @@
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: flex;
align-items: center;
justify-content: center;
}
.emoji-picker__emoji img.emoji {

4
index.d.ts

@ -34,6 +34,7 @@ declare namespace EmojiButton {
rows?: number;
emojiSize?: string;
initialCategory?: Category | 'recents';
custom?: EmojiRecord[];
}
export type EmojiStyle = 'native' | 'twemoji';
@ -88,7 +89,8 @@ declare namespace EmojiButton {
| 'travel'
| 'objects'
| 'symbols'
| 'flags';
| 'flags'
| 'custom';
export interface I18NStrings {
search: string;

9
src/categoryButtons.ts

@ -21,7 +21,8 @@ const categoryIcons: { [key in I18NCategory]: string } = {
travel: icons.building,
objects: icons.lightbulb,
symbols: icons.music,
flags: icons.flag
flags: icons.flag,
custom: icons.icons
};
export class CategoryButtons {
@ -38,10 +39,14 @@ export class CategoryButtons {
render(): HTMLElement {
const container = createElement('div', CLASS_CATEGORY_BUTTONS);
const categories = this.options.showRecents
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);
button.innerHTML = categoryIcons[category];

1
src/classes.ts

@ -1,6 +1,7 @@
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';

38
src/emoji.ts

@ -6,7 +6,7 @@ import { smile } from './icons';
import { save } from './recent';
import { createElement } from './util';
import { CLASS_EMOJI } from './classes';
import { CLASS_EMOJI, CLASS_CUSTOM_EMOJI } from './classes';
import { EmojiButtonOptions, EmojiRecord } from './types';
@ -24,15 +24,41 @@ export class Emoji {
render(): HTMLElement {
this.emojiButton = createElement('button', CLASS_EMOJI);
this.emojiButton.innerHTML =
this.options.style === 'native'
? this.emoji.emoji
: this.lazy
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
: twemoji.parse(this.emoji.emoji);
: `<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.emojiButton.innerHTML = content;
// this.options.style === 'native'
// ? this.emoji.emoji
// : this.lazy
// ? smile
// : twemoji.parse(this.emoji.emoji);
this.emojiButton.tabIndex = -1;
this.emojiButton.dataset.emoji = this.emoji.emoji;
if (this.emoji.custom) {
this.emojiButton.dataset.custom = 'true';
}
this.emojiButton.title = this.emoji.name;
this.emojiButton.addEventListener('focus', () => this.onEmojiHover());

38
src/emojiArea.ts

@ -58,6 +58,10 @@ export class EmojiArea {
if (options.showRecents) {
this.categories = ['recents', ...this.categories];
}
if (options.custom) {
this.categories = [...this.categories, 'custom'];
}
}
updateRecents(): void {
@ -99,6 +103,13 @@ export class EmojiArea {
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])
);
@ -172,13 +183,13 @@ export class EmojiArea {
if (
this.focusedIndex === this.currentEmojiCount - 1 &&
this.currentCategory < this.categories.length
this.currentCategory < this.categories.length - 1
) {
if (this.options.showCategoryButtons) {
this.categoryButtons.setActiveButton(++this.currentCategory);
}
this.setFocusedEmoji(0);
} else {
} else if (this.focusedIndex < this.currentEmojiCount - 1) {
this.setFocusedEmoji(this.focusedIndex + 1);
}
break;
@ -200,14 +211,22 @@ export class EmojiArea {
if (
this.focusedIndex + this.emojisPerRow >= this.currentEmojiCount &&
this.currentCategory < this.categories.length
this.currentCategory < this.categories.length - 1
) {
this.currentCategory++;
if (this.options.showCategoryButtons) {
this.categoryButtons.setActiveButton(this.currentCategory);
}
this.setFocusedEmoji(this.focusedIndex % this.emojisPerRow);
} else {
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;
@ -310,9 +329,16 @@ export class EmojiArea {
return;
}
let closestHeaderIndex = this.headerOffsets.findIndex(
offset => offset > Math.round(this.emojis.scrollTop)
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) {

3
src/i18n.ts

@ -12,7 +12,8 @@ export const i18n: I18NStrings = {
travel: 'Travel & Places',
objects: 'Objects',
symbols: 'Symbols',
flags: 'Flags'
flags: 'Flags',
custom: 'Custom'
},
notFound: 'No emojis found'
};

3
src/icons.ts

@ -4,6 +4,7 @@ import {
faCoffee,
faFutbol,
faHistory,
faIcons,
faMusic,
faSearch,
faTimes,
@ -25,6 +26,7 @@ library.add(
faFrown,
faFutbol,
faHistory,
faIcons,
faLightbulb,
faMusic,
faSearch,
@ -40,6 +42,7 @@ 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];

75
src/index.ts

@ -27,7 +27,8 @@ import {
CLASS_SEARCH_FIELD,
CLASS_VARIANT_OVERLAY,
CLASS_WRAPPER,
CLASS_OVERLAY
CLASS_OVERLAY,
CLASS_CUSTOM_EMOJI
} from './classes';
import { EmojiButtonOptions, I18NStrings, EmojiRecord } from './types';
@ -212,13 +213,18 @@ export class EmojiButton {
setTimeout(() => this.emojiArea.updateRecents());
if (this.options.style === 'twemoji') {
if (emoji.custom) {
this.publicEvents.emit(
'emoji',
EMOJI,
`<img class="emoji" src="${emoji.emoji}">`
);
} else if (this.options.style === 'twemoji') {
this.publicEvents.emit(
EMOJI,
twemoji.parse(emoji.emoji, twemojiOptions)
);
} else {
this.publicEvents.emit('emoji', emoji.emoji);
this.publicEvents.emit(EMOJI, emoji.emoji);
}
if (this.options.autoHide) {
this.hidePicker();
@ -239,7 +245,7 @@ export class EmojiButton {
this.options.rootElement.appendChild(this.wrapper);
}
this.observeTwemojiForLazyLoad();
this.observeForLazyLoad();
}
private showVariantPopup(emoji: EmojiRecord) {
@ -265,17 +271,27 @@ export class EmojiButton {
});
}
private observeTwemojiForLazyLoad() {
if (this.options.style === 'twemoji') {
const onChange = changes => {
const visibleElements = Array.prototype.filter
.call(changes, change => {
return change.intersectionRatio > 0;
})
.map(entry => entry.target);
visibleElements.forEach(element => {
if (this.options.style === 'twemoji' && !element.dataset.loaded) {
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,
twemojiOptions
@ -283,21 +299,26 @@ export class EmojiButton {
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}`
);
this.observer = new IntersectionObserver(onChange, {
root: this.emojiArea.emojis
});
const emojiElements = this.emojiArea.emojis.querySelectorAll(
`.${CLASS_EMOJI}`
);
emojiElements.forEach(element => {
emojiElements.forEach(element => {
if (
this.options.style === 'twemoji' ||
(element as HTMLElement).dataset.custom === 'true'
) {
this.observer.observe(element);
});
}
}
});
}
private onDocumentClick(event: MouseEvent): void {

16
src/preview.ts

@ -9,7 +9,8 @@ import { EmojiRecord, EmojiButtonOptions } from './types';
import {
CLASS_PREVIEW,
CLASS_PREVIEW_EMOJI,
CLASS_PREVIEW_NAME
CLASS_PREVIEW_NAME,
CLASS_CUSTOM_EMOJI
} from './classes';
const twemojiOptions = {
@ -41,10 +42,15 @@ export class EmojiPreview {
}
showPreview(emoji: EmojiRecord): void {
this.emoji.innerHTML =
this.options.style === 'native'
? emoji.emoji
: twemoji.parse(emoji.emoji, twemojiOptions);
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, twemojiOptions);
}
this.emoji.innerHTML = content;
this.name.innerHTML = emoji.name;
}

3
src/recent.ts

@ -17,7 +17,8 @@ export function save(
const recent = {
emoji: emoji.emoji,
name: emoji.name,
key: (emoji as RecentEmoji).key || emoji.name
key: (emoji as RecentEmoji).key || emoji.name,
custom: emoji.custom
};
localStorage.setItem(

6
src/types.ts

@ -3,6 +3,7 @@ import { Placement } from '@popperjs/core';
export interface EmojiRecord {
name: string;
emoji: string;
custom?: boolean;
category?: number;
version?: string;
variations?: string[];
@ -18,6 +19,7 @@ export interface RecentEmoji {
key: string;
name: string;
emoji: string;
custom?: boolean;
}
export interface EmojiEventData {
@ -47,6 +49,7 @@ export interface EmojiButtonOptions {
rows?: number;
emojiSize?: string;
initialCategory?: Category | 'recents';
custom?: EmojiRecord[];
}
export type EmojiStyle = 'native' | 'twemoji';
@ -84,7 +87,8 @@ export type I18NCategory =
| 'travel'
| 'objects'
| 'symbols'
| 'flags';
| 'flags'
| 'custom';
export interface I18NStrings {
search: string;

Loading…
Cancel
Save