You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
372 lines
10 KiB
372 lines
10 KiB
import { TinyEmitter as Emitter } from 'tiny-emitter';
|
|
import escape from 'escape-html';
|
|
|
|
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,
|
|
EmojiButtonOptions,
|
|
EmojiRecord,
|
|
RecentEmoji
|
|
} from './types';
|
|
import { createElement } from './util';
|
|
import { load } from './recent';
|
|
|
|
const categorySortOrder = [
|
|
'recents',
|
|
'smileys',
|
|
'people',
|
|
'animals',
|
|
'food',
|
|
'activities',
|
|
'travel',
|
|
'objects',
|
|
'symbols',
|
|
'flags',
|
|
'custom'
|
|
];
|
|
|
|
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: EmojiButtonOptions,
|
|
private emojiCategories: { [key: string]: EmojiRecord[] }
|
|
) {
|
|
this.emojisPerRow = options.emojisPerRow || 8;
|
|
this.categories =
|
|
options.emojiData?.categories ||
|
|
options.categories ||
|
|
emojiData.categories;
|
|
|
|
if (options.showRecents) {
|
|
this.categories = ['recents', ...this.categories];
|
|
}
|
|
|
|
if (options.custom) {
|
|
this.categories = [...this.categories, 'custom'];
|
|
}
|
|
|
|
this.categories.sort(
|
|
(a, b) => categorySortOrder.indexOf(a) - categorySortOrder.indexOf(b)
|
|
);
|
|
}
|
|
|
|
updateRecents(): void {
|
|
if (this.options.showRecents) {
|
|
this.emojiCategories.recents = load();
|
|
const recentsContainer = this.emojis.querySelector(
|
|
`.${CLASS_EMOJI_CONTAINER}`
|
|
) as HTMLElement;
|
|
if (recentsContainer && recentsContainer.parentNode) {
|
|
recentsContainer.parentNode.replaceChild(
|
|
new EmojiContainer(
|
|
this.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) {
|
|
this.emojiCategories.recents = load();
|
|
}
|
|
|
|
if (this.options.custom) {
|
|
this.emojiCategories.custom = this.options.custom.map(custom => ({
|
|
...custom,
|
|
custom: true
|
|
}));
|
|
}
|
|
|
|
this.categories.forEach(category =>
|
|
this.addCategory(category, this.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 = escape(
|
|
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);
|
|
}
|
|
};
|
|
}
|
|
|