Browse Source

Convert to TypeScript

master
Joe Attardi 4 years ago
parent
commit
26bc41edc9
  1. 3
      .babelrc
  2. 4
      index.d.ts
  3. 1633
      package-lock.json
  4. 12
      package.json
  5. 8
      rollup.config.js
  6. 6
      scripts/processEmojiData.js
  7. 2
      src/data/emoji.js
  8. 0
      src/emoji.test.ts
  9. 23
      src/emoji.ts
  10. 0
      src/emojiContainer.test.ts
  11. 15
      src/emojiContainer.ts
  12. 0
      src/events.ts
  13. 4
      src/i18n.ts
  14. 24
      src/icons.ts
  15. 85
      src/index.ts
  16. 0
      src/preview.test.ts
  17. 18
      src/preview.ts
  18. 8
      src/recent.ts
  19. 0
      src/search.test.ts
  20. 117
      src/search.ts
  21. 0
      src/tabs.test.ts
  22. 95
      src/tabs.ts
  23. 53
      src/types.ts
  24. 8
      src/util.ts
  25. 0
      src/variantPopup.test.ts
  26. 22
      src/variantPopup.ts
  27. 67
      tsconfig.json

3
.babelrc

@ -1,3 +1,4 @@
{
"presets": ["@babel/preset-env"]
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
"plugins": ["transform-class-properties"]
}

4
index.d.ts

@ -0,0 +1,4 @@
declare class EmojiButton {
constructor(options: EmojiButtonOptions);
}

1633
package-lock.json

File diff suppressed because it is too large

12
package.json

@ -27,7 +27,12 @@
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.8.3",
"@rollup/plugin-replace": "^2.3.0",
"@rollup/plugin-typescript": "^3.0.0",
"@types/jest": "^25.1.3",
"babel-jest": "^25.1.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"eslint": "^6.4.0",
"eslint-config-prettier": "^6.3.0",
"eslint-plugin-prettier": "^3.1.0",
@ -37,7 +42,9 @@
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-postcss": "^2.0.3"
"rollup-plugin-postcss": "^2.0.3",
"ts-jest": "^25.2.1",
"typescript": "^3.7.5"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.22",
@ -46,7 +53,8 @@
"@popperjs/core": "^2.0.0",
"emoji-datasource": "^5.0.1",
"focus-trap": "^5.1.0",
"tiny-emitter": "^2.1.0"
"tiny-emitter": "^2.1.0",
"tslib": "^1.10.0"
},
"files": [
"dist"

8
rollup.config.js

@ -1,13 +1,13 @@
const babel = require('rollup-plugin-babel');
const commonjs = require('rollup-plugin-commonjs');
const postcss = require('rollup-plugin-postcss');
const resolve = require('rollup-plugin-node-resolve');
const replace = require('@rollup/plugin-replace');
const typescript = require('@rollup/plugin-typescript');
const production = process.env.NODE_ENV === 'production';
module.exports = {
input: 'src/index.js',
input: 'src/index.ts',
output: {
file: 'dist/index.js',
format: 'umd',
@ -20,9 +20,7 @@ module.exports = {
postcss({
extensions: ['.css']
}),
babel({
compact: production
}),
typescript(),
resolve(),
commonjs()
]

6
scripts/processEmojiData.js

@ -58,7 +58,5 @@ const newEmojiData = rawData.map(emojiItem => {
writeFileSync(
'src/data/emoji.js',
`export const categories = ${JSON.stringify(
categories.map(category => categoryKeys[category])
)}; export default ${JSON.stringify(newEmojiData)};`
);
`export default { categories: ${JSON.stringify(categories.map(category => categoryKeys[category]))}, emojiData: ${JSON.stringify(newEmojiData)}}`
);

2
src/data/emoji.js

File diff suppressed because one or more lines are too long

0
src/emoji.test.js → src/emoji.test.ts

23
src/emoji.js → src/emoji.ts

@ -1,19 +1,20 @@
import { TinyEmitter as Emitter } from 'tiny-emitter';
import { EMOJI, HIDE_PREVIEW, SHOW_PREVIEW } from './events';
import { save } from './recent';
import { createElement } from './util';
import { EmojiButtonOptions, EmojiRecord, EmojiVariation } from './types';
const CLASS_EMOJI = 'emoji-picker__emoji';
export class Emoji {
constructor(emoji, showVariants, showPreview, events, options) {
this.emoji = emoji;
this.showVariants = showVariants;
this.showPreview = showPreview;
this.events = events;
this.options = options;
private emojiButton: HTMLElement;
constructor(private emoji: EmojiRecord | EmojiVariation, private showVariants: boolean, private showPreview: boolean, private events: Emitter, private options: EmojiButtonOptions) {
}
render() {
render(): HTMLElement {
this.emojiButton = createElement('button', CLASS_EMOJI);
this.emojiButton.innerHTML = this.emoji.e;
this.emojiButton.tabIndex = -1;
@ -27,10 +28,10 @@ export class Emoji {
return this.emojiButton;
}
onEmojiClick() {
onEmojiClick(): void {
// TODO move this side effect out of Emoji, make the recent module listen for event
if (
(!this.emoji.v || !this.showVariants || !this.options.showVariants) &&
(!(this.emoji as EmojiRecord).v || !this.showVariants || !this.options.showVariants) &&
this.options.showRecents
) {
save(this.emoji, this.options);
@ -43,13 +44,13 @@ export class Emoji {
});
}
onEmojiHover() {
onEmojiHover(): void {
if (this.showPreview) {
this.events.emit(SHOW_PREVIEW, this.emoji);
}
}
onEmojiLeave() {
onEmojiLeave(): void {
if (this.showPreview) {
this.events.emit(HIDE_PREVIEW);
}

0
src/emojiContainer.test.js → src/emojiContainer.test.ts

15
src/emojiContainer.js → src/emojiContainer.ts

@ -1,19 +1,22 @@
import { TinyEmitter as Emitter } from 'tiny-emitter';
import { Emoji } from './emoji';
import { createElement } from './util';
import { EmojiButtonOptions, EmojiRecord } from './types';
const CLASS_EMOJI_CONTAINER = 'emoji-picker__emojis';
export class EmojiContainer {
constructor(emojis, showVariants, events, options) {
private emojis: EmojiRecord[];
constructor(emojis: EmojiRecord[], private showVariants: boolean, private events: Emitter, private options: EmojiButtonOptions) {
this.emojis = emojis.filter(
e => !e.ver || e.ver <= parseFloat(options.emojiVersion)
e => !e.ver || parseFloat(e.ver) <= parseFloat(options.emojiVersion as string)
);
this.showVariants = showVariants;
this.events = events;
this.options = options;
}
render() {
render(): HTMLElement {
const emojiContainer = createElement('div', CLASS_EMOJI_CONTAINER);
this.emojis.forEach(emoji =>
emojiContainer.appendChild(

0
src/events.js → src/events.ts

4
src/i18n.js → src/i18n.ts

@ -1,4 +1,6 @@
export const i18n = {
import { I18NStrings } from './types';
export const i18n: I18NStrings = {
search: 'Search emojis...',
categories: {
recents: 'Recent Emojis',

24
src/icons.js → src/icons.ts

@ -31,15 +31,15 @@ library.add(
faTimes
);
export const building = icon({ prefix: 'far', iconName: 'building' }).html;
export const cat = icon({ prefix: 'fas', iconName: 'cat' }).html;
export const coffee = icon({ prefix: 'fas', iconName: 'coffee' }).html;
export const flag = icon({ prefix: 'far', iconName: 'flag' }).html;
export const futbol = icon({ prefix: 'fas', iconName: 'futbol' }).html;
export const frown = icon({ prefix: 'far', iconName: 'frown' }).html;
export const history = icon({ prefix: 'fas', iconName: 'history' }).html;
export const lightbulb = icon({ prefix: 'far', iconName: 'lightbulb' }).html;
export const music = icon({ prefix: 'fas', iconName: 'music' }).html;
export const search = icon({ prefix: 'fas', iconName: 'search' }).html;
export const smile = icon({ prefix: 'far', iconName: 'smile' }).html;
export const times = icon({ prefix: 'fas', iconName: 'times' }).html;
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 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];

85
src/index.js → src/index.ts

@ -1,8 +1,8 @@
import '../css/emoji-button.css';
import createFocusTrap from 'focus-trap';
import Emitter from 'tiny-emitter';
import { createPopper } from '@popperjs/core';
import createFocusTrap, { FocusTrap } from 'focus-trap';
import { TinyEmitter as Emitter } from 'tiny-emitter';
import { createPopper, Instance as Popper } from '@popperjs/core';
import emojiData from './data/emoji';
@ -21,10 +21,12 @@ import { VariantPopup } from './variantPopup';
import { i18n } from './i18n';
import { EmojiButtonOptions, I18NStrings } from './types';
const CLASS_PICKER = 'emoji-picker';
const CLASS_PICKER_CONTENT = 'emoji-picker__content';
const DEFAULT_OPTIONS = {
const DEFAULT_OPTIONS: EmojiButtonOptions = {
position: 'right-start',
autoHide: true,
autoFocusSearch: true,
@ -37,7 +39,22 @@ const DEFAULT_OPTIONS = {
};
export default class EmojiButton {
constructor(options = {}) {
pickerVisible: boolean;
private events = new Emitter();
private publicEvents = new Emitter();
private options: EmojiButtonOptions;
private i18n: I18NStrings;
private pickerEl: HTMLElement;
private focusTrap: FocusTrap;
private hideInProgress: boolean;
private destroyTimeout: NodeJS.Timeout;
private popper: Popper;
constructor(options: EmojiButtonOptions = {}) {
this.pickerVisible = false;
this.options = { ...DEFAULT_OPTIONS, ...options };
@ -52,27 +69,24 @@ export default class EmojiButton {
this.onDocumentClick = this.onDocumentClick.bind(this);
this.onDocumentKeydown = this.onDocumentKeydown.bind(this);
this.events = new Emitter();
this.publicEvents = new Emitter();
}
on(event, callback) {
on(event: string, callback: Function): void {
this.publicEvents.on(event, callback);
}
off(event, callback) {
off(event: string, callback: Function): void {
this.publicEvents.off(event, callback);
}
buildPicker() {
buildPicker(): void {
this.pickerEl = createElement('div', CLASS_PICKER);
this.focusTrap = createFocusTrap(this.pickerEl, {
this.focusTrap = createFocusTrap(<HTMLElement> this.pickerEl, {
clickOutsideDeactivates: true
});
if (this.options.zIndex) {
this.pickerEl.style.zIndex = this.options.zIndex;
this.pickerEl.style.zIndex = this.options.zIndex + '';
}
const pickerContent = createElement('div', CLASS_PICKER_CONTENT);
@ -82,8 +96,8 @@ export default class EmojiButton {
this.events,
this.i18n,
this.options,
emojiData,
this.options.autoFocusSearch
emojiData.emojiData,
this.options.autoFocusSearch || true
).render();
this.pickerEl.appendChild(searchContainer);
}
@ -106,7 +120,7 @@ export default class EmojiButton {
}
});
this.events.on(SHOW_SEARCH_RESULTS, searchResults => {
this.events.on(SHOW_SEARCH_RESULTS, (searchResults: HTMLElement) => {
empty(pickerContent);
searchResults.classList.add('search-results');
pickerContent.appendChild(searchResults);
@ -116,15 +130,18 @@ export default class EmojiButton {
this.pickerEl.appendChild(new EmojiPreview(this.events).render());
}
let variantPopup;
this.events.on(EMOJI, ({ emoji, showVariants }) => {
let variantPopup: HTMLElement | null;
this.events.on(EMOJI, ({ emoji, showVariants }: { emoji: any, showVariants: boolean }) => {
if (emoji.v && showVariants && this.options.showVariants) {
variantPopup = new VariantPopup(
this.events,
emoji,
this.options
).render();
this.pickerEl.appendChild(variantPopup);
if (variantPopup) {
this.pickerEl.appendChild(variantPopup);
}
} else {
if (variantPopup && variantPopup.parentNode === this.pickerEl) {
this.pickerEl.removeChild(variantPopup);
@ -137,11 +154,15 @@ export default class EmojiButton {
});
this.events.on(HIDE_VARIANT_POPUP, () => {
this.pickerEl.removeChild(variantPopup);
if (variantPopup) {
this.pickerEl.removeChild(variantPopup);
}
variantPopup = null;
});
this.options.rootElement.appendChild(this.pickerEl);
if (this.options.rootElement) {
this.options.rootElement.appendChild(this.pickerEl);
}
setTimeout(() => {
document.addEventListener('click', this.onDocumentClick);
@ -149,20 +170,22 @@ export default class EmojiButton {
});
}
onDocumentClick(event) {
if (!this.pickerEl.contains(event.target)) {
onDocumentClick(event: MouseEvent): void {
if (!this.pickerEl.contains(<Node> event.target)) {
this.hidePicker();
}
}
destroyPicker() {
this.options.rootElement.removeChild(this.pickerEl);
this.popper.destroy();
this.pickerEl.style.transition = '';
this.hideInProgress = false;
destroyPicker(): void {
if (this.options.rootElement) {
this.options.rootElement.removeChild(this.pickerEl);
this.popper.destroy();
this.pickerEl.style.transition = '';
this.hideInProgress = false;
}
}
hidePicker() {
hidePicker(): void {
this.focusTrap.deactivate();
this.pickerEl.classList.remove('visible');
this.pickerVisible = false;
@ -176,7 +199,7 @@ export default class EmojiButton {
document.removeEventListener('keydown', this.onDocumentKeydown);
}
showPicker(referenceEl, options = {}) {
showPicker(referenceEl: HTMLElement, options: EmojiButtonOptions = {}): void {
if (this.hideInProgress) {
clearTimeout(this.destroyTimeout);
this.destroyPicker();
@ -192,7 +215,7 @@ export default class EmojiButton {
requestAnimationFrame(() => this.pickerEl.classList.add('visible'));
}
onDocumentKeydown(event) {
onDocumentKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
this.hidePicker();
}

0
src/preview.test.js → src/preview.test.ts

18
src/preview.js → src/preview.ts

@ -1,16 +1,20 @@
import { TinyEmitter as Emitter } from 'tiny-emitter';
import { SHOW_PREVIEW, HIDE_PREVIEW } from './events';
import { createElement, getEmojiName } from './util';
import { EmojiRecord } from './types';
const CLASS_PREVIEW = 'emoji-picker__preview';
const CLASS_PREVIEW_EMOJI = 'emoji-picker__preview-emoji';
const CLASS_PREVIEW_NAME = 'emoji-picker__preview-name';
export class EmojiPreview {
constructor(events) {
this.events = events;
}
private emoji: HTMLElement;
private name: HTMLElement;
constructor(private events: Emitter) {}
render() {
render(): HTMLElement {
const preview = createElement('div', CLASS_PREVIEW);
this.emoji = createElement('div', CLASS_PREVIEW_EMOJI);
@ -19,18 +23,18 @@ export class EmojiPreview {
this.name = createElement('div', CLASS_PREVIEW_NAME);
preview.appendChild(this.name);
this.events.on(SHOW_PREVIEW, emoji => this.showPreview(emoji));
this.events.on(SHOW_PREVIEW, (emoji: EmojiRecord) => this.showPreview(emoji));
this.events.on(HIDE_PREVIEW, () => this.hidePreview());
return preview;
}
showPreview(emoji) {
showPreview(emoji: EmojiRecord): void {
this.emoji.innerHTML = emoji.e;
this.name.innerHTML = getEmojiName(emoji);
}
hidePreview() {
hidePreview(): void {
this.emoji.innerHTML = '';
this.name.innerHTML = '';
}

8
src/recent.js → src/recent.ts

@ -1,12 +1,14 @@
import { getEmojiName } from './util';
import { EmojiData, EmojiButtonOptions, RecentEmoji } from './types';
const LOCAL_STORAGE_KEY = 'emojiPicker.recent';
export function load() {
return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [];
const recentJson = localStorage.getItem(LOCAL_STORAGE_KEY);
return recentJson ? JSON.parse(recentJson) : [];
}
export function save(emoji, options) {
export function save(emoji: any, options: EmojiButtonOptions): void {
const recents = load();
const recent = {
@ -18,7 +20,7 @@ export function save(emoji, options) {
localStorage.setItem(
LOCAL_STORAGE_KEY,
JSON.stringify(
[recent, ...recents.filter(r => r.k !== recent.k)].slice(
[recent, ...recents.filter((r: RecentEmoji) => r.k !== recent.k)].slice(
0,
options.recentsCount
)

0
src/search.test.js → src/search.test.ts

117
src/search.js → src/search.ts

@ -1,3 +1,5 @@
import { TinyEmitter as Emitter } from 'tiny-emitter';
import * as icons from './icons';
import { EmojiContainer } from './emojiContainer';
@ -9,6 +11,7 @@ import {
SHOW_TABS
} from './events';
import { createElement } from './util';
import { I18NStrings, EmojiButtonOptions, EmojiData, EmojiRecord } from './types';
const CLASS_SEARCH_CONTAINER = 'emoji-picker__search-container';
const CLASS_SEARCH_FIELD = 'emoji-picker__search';
@ -19,12 +22,18 @@ const CLASS_NOT_FOUND_ICON = 'emoji-picker__search-not-found-icon';
const EMOJIS_PER_ROW = 8;
export class Search {
constructor(events, i18n, options, emojiData, autoFocusSearch) {
this.events = events;
this.i18n = i18n;
private emojiData: EmojiRecord[];
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: EmojiButtonOptions, emojiData: EmojiRecord[], private autoFocusSearch: boolean) {
this.options = options;
this.emojiData = emojiData.filter(
e => e.ver <= parseFloat(options.emojiVersion)
e => parseFloat(e.ver) <= parseFloat(options.emojiVersion as string)
);
this.autoFocusSearch = autoFocusSearch;
@ -33,11 +42,11 @@ export class Search {
});
}
render() {
render(): HTMLElement {
this.searchContainer = createElement('div', CLASS_SEARCH_CONTAINER);
this.searchField = createElement('input', CLASS_SEARCH_FIELD);
this.searchField.placeholder = this.i18n.search;
this.searchField = <HTMLInputElement> createElement('input', CLASS_SEARCH_FIELD);
this.searchField.placeholder = <string> this.i18n.search;
this.searchContainer.appendChild(this.searchField);
this.searchIcon = createElement('span', CLASS_SEARCH_ICON);
@ -60,7 +69,7 @@ export class Search {
return this.searchContainer;
}
onClearSearch(event) {
onClearSearch(event: Event): void {
event.stopPropagation();
if (this.searchField.value) {
@ -74,51 +83,55 @@ export class Search {
}
}
setFocusedEmoji(index) {
const emojis = this.resultsContainer.querySelectorAll(
'.emoji-picker__emoji'
);
const currentFocusedEmoji = emojis[this.focusedEmojiIndex];
currentFocusedEmoji.tabIndex = -1;
setFocusedEmoji(index: number): void {
if (this.resultsContainer) {
const emojis = this.resultsContainer.querySelectorAll(
'.emoji-picker__emoji'
);
const currentFocusedEmoji = <HTMLElement> emojis[this.focusedEmojiIndex];
currentFocusedEmoji.tabIndex = -1;
this.focusedEmojiIndex = index;
const newFocusedEmoji = emojis[this.focusedEmojiIndex];
newFocusedEmoji.tabIndex = 0;
newFocusedEmoji.focus();
this.focusedEmojiIndex = index;
const newFocusedEmoji = <HTMLElement> emojis[this.focusedEmojiIndex];
newFocusedEmoji.tabIndex = 0;
newFocusedEmoji.focus();
}
}
handleResultsKeydown(event) {
const emojis = this.resultsContainer.querySelectorAll(
'.emoji-picker__emoji'
);
if (event.key === 'ArrowRight') {
this.setFocusedEmoji(
Math.min(this.focusedEmojiIndex + 1, emojis.length - 1)
handleResultsKeydown(event: KeyboardEvent): void {
if (this.resultsContainer) {
const emojis = this.resultsContainer.querySelectorAll(
'.emoji-picker__emoji'
);
} 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 - EMOJIS_PER_ROW) {
this.setFocusedEmoji(this.focusedEmojiIndex + EMOJIS_PER_ROW);
}
} else if (event.key === 'ArrowUp') {
event.preventDefault();
if (this.focusedEmojiIndex >= EMOJIS_PER_ROW) {
this.setFocusedEmoji(this.focusedEmojiIndex - EMOJIS_PER_ROW);
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 - EMOJIS_PER_ROW) {
this.setFocusedEmoji(this.focusedEmojiIndex + EMOJIS_PER_ROW);
}
} else if (event.key === 'ArrowUp') {
event.preventDefault();
if (this.focusedEmojiIndex >= EMOJIS_PER_ROW) {
this.setFocusedEmoji(this.focusedEmojiIndex - EMOJIS_PER_ROW);
}
} else if (event.key === 'Escape') {
this.onClearSearch(event);
}
} else if (event.key === 'Escape') {
this.onClearSearch(event);
}
}
onKeyDown(event) {
onKeyDown(event: KeyboardEvent): void {
if (event.key === 'Escape' && this.searchField.value) {
this.onClearSearch(event);
}
}
onKeyUp() {
onKeyUp(): void {
if (!this.searchField.value) {
this.searchIcon.innerHTML = icons.search;
this.searchIcon.style.cursor = 'default';
@ -149,16 +162,18 @@ export class Search {
this.options
).render();
this.resultsContainer.querySelector(
'.emoji-picker__emoji'
).tabIndex = 0;
this.focusedEmojiIndex = 0;
if (this.resultsContainer) {
(<HTMLElement>this.resultsContainer.querySelector(
'.emoji-picker__emoji'
)).tabIndex = 0;
this.focusedEmojiIndex = 0;
this.resultsContainer.addEventListener('keydown', event =>
this.handleResultsKeydown(event)
);
this.resultsContainer.addEventListener('keydown', event =>
this.handleResultsKeydown(event)
);
this.events.emit(SHOW_SEARCH_RESULTS, this.resultsContainer);
this.events.emit(SHOW_SEARCH_RESULTS, this.resultsContainer);
}
} else {
this.events.emit(
SHOW_SEARCH_RESULTS,
@ -170,11 +185,9 @@ export class Search {
}
class NotFoundMessage {
constructor(message) {
this.message = message;
}
constructor(private message: string) {}
render() {
render(): HTMLElement {
const container = createElement('div', CLASS_NOT_FOUND);
const iconContainer = createElement('div', CLASS_NOT_FOUND_ICON);

0
src/tabs.test.js → src/tabs.test.ts

95
src/tabs.js → src/tabs.ts

@ -1,4 +1,6 @@
import emojiData, { categories } from './data/emoji.js';
import { TinyEmitter as Emitter } from 'tiny-emitter';
import emojiData from './data/emoji';
import { EmojiContainer } from './emojiContainer';
import { EMOJI, HIDE_VARIANT_POPUP } from './events';
@ -6,6 +8,7 @@ import { load } from './recent';
import { i18n as defaultI18n } from './i18n';
import * as icons from './icons';
import { createElement } from './util';
import { EmojiRecord, I18NStrings, EmojiButtonOptions, I18NCategory } from './types.js';
const CLASS_ACTIVE_TAB = 'active';
const CLASS_TABS_CONTAINER = 'emoji-picker__tabs-container';
@ -15,8 +18,10 @@ const CLASS_TAB_BODY = 'emoji-picker__tab-body';
const EMOJIS_PER_ROW = 8;
const emojiCategories = {};
emojiData.forEach(emoji => {
const categories = emojiData.categories;
const emojiCategories: { [key: string] : EmojiRecord[]} = {};
emojiData.emojiData.forEach(emoji => {
let categoryList = emojiCategories[categories[emoji.c]];
if (!categoryList) {
categoryList = emojiCategories[categories[emoji.c]] = [];
@ -25,7 +30,7 @@ emojiData.forEach(emoji => {
categoryList.push(emoji);
});
const categoryIcons = {
const categoryIcons: { [key in I18NCategory]: string } = {
smileys: icons.smile,
animals: icons.cat,
food: icons.coffee,
@ -33,19 +38,25 @@ const categoryIcons = {
travel: icons.building,
objects: icons.lightbulb,
symbols: icons.music,
flags: icons.flag
flags: icons.flag,
recents: icons.history
};
export class Tabs {
constructor(events, i18n, options) {
this.events = events;
this.i18n = i18n;
this.options = options;
private activeTab: number;
private tabBodies: TabBody[];
private tabs: Tab[];
private tabsList: HTMLElement;
private tabBodyContainer: HTMLElement;
private focusedEmojiIndex = 0;
constructor(private events: Emitter, private i18n: I18NStrings, private options: EmojiButtonOptions) {
this.setActiveTab = this.setActiveTab.bind(this);
}
setActiveTab(index, animate = true) {
setActiveTab(index: number, animate = true) {
if (index === this.activeTab) {
return;
}
@ -60,18 +71,22 @@ export class Tabs {
const currentActiveTabBody = this.tabBodies[currentActiveTab].container;
currentActiveTabBody
.querySelectorAll('.emoji-picker__emoji')
.forEach(emoji => (emoji.tabIndex = -1));
.forEach((emoji: Element) => ((<HTMLElement>emoji).tabIndex = -1));
const activeEmojiContainer = newActiveTabBody.querySelector(
'.emoji-picker__emojis'
);
activeEmojiContainer.scrollTop = 0;
const firstEmoji = activeEmojiContainer.querySelector(
'.emoji-picker__emoji'
);
if (firstEmoji) {
firstEmoji.tabIndex = 0;
if (activeEmojiContainer) {
activeEmojiContainer.scrollTop = 0;
const firstEmoji = activeEmojiContainer.querySelector(
'.emoji-picker__emoji'
);
if (firstEmoji) {
(<HTMLElement>firstEmoji).tabIndex = 0;
}
}
this.focusedEmojiIndex = 0;
if (animate) {
@ -89,10 +104,10 @@ export class Tabs {
}
transitionTabs(
newActiveTabBody,
currentActiveTabBody,
newTranslate,
currentTranslate
newActiveTabBody: HTMLElement,
currentActiveTabBody: HTMLElement,
newTranslate: number,
currentTranslate: number
) {
requestAnimationFrame(() => {
newActiveTabBody.style.transition = 'none';
@ -118,22 +133,22 @@ export class Tabs {
'.emoji-picker__emoji'
);
if (firstEmoji) {
firstEmoji.tabIndex = 0;
(<HTMLElement>firstEmoji).tabIndex = 0;
}
this.focusedEmojiIndex = 0;
return tabsContainer;
}
setFocusedEmoji(index) {
setFocusedEmoji(index: number) {
const emojis = this.tabBodies[this.activeTab].content.querySelectorAll(
'.emoji-picker__emoji'
);
const currentFocusedEmoji = emojis[this.focusedEmojiIndex];
const currentFocusedEmoji = <HTMLElement> emojis[this.focusedEmojiIndex];
currentFocusedEmoji.tabIndex = -1;
this.focusedEmojiIndex = index;
const newFocusedEmoji = emojis[this.focusedEmojiIndex];
const newFocusedEmoji = <HTMLElement> emojis[this.focusedEmojiIndex];
newFocusedEmoji.tabIndex = 0;
newFocusedEmoji.focus();
}
@ -173,11 +188,11 @@ export class Tabs {
this.tabBodyContainer = createElement('div');
this.tabBodies = Object.keys(categoryIcons).map(
(category, index) =>
(category: string, index: number) =>
new TabBody(
this.i18n.categories[category] || defaultI18n.categories[category],
new EmojiContainer(
emojiCategories[category],
emojiCategories[category] || [],
true,
this.events,
this.options
@ -214,8 +229,8 @@ export class Tabs {
setTimeout(() => this.setFocusedEmoji(this.focusedEmojiIndex));
});
this.events.on(EMOJI, ({ button }) => {
if (button.parentElement.classList.contains('emoji-picker__emojis')) {
this.events.on(EMOJI, ({ button }: { button: HTMLButtonElement }) => {
if (button.parentElement && button.parentElement.classList.contains('emoji-picker__emojis')) {
this.setFocusedEmoji(
Array.prototype.indexOf.call(button.parentElement.children, button)
);
@ -247,7 +262,7 @@ export class Tabs {
setTimeout(() => {
this.tabBodyContainer.replaceChild(
newRecentsEl,
this.tabBodyContainer.firstChild
<Node> this.tabBodyContainer.firstChild
);
this.tabBodies[0] = newRecents;
@ -267,11 +282,9 @@ export class Tabs {
}
class Tab {
constructor(icon, index, setActiveTab) {
this.icon = icon;
this.index = index;
this.setActiveTab = setActiveTab;
}
tab: HTMLElement;
constructor(private icon: string, private index: number, private setActiveTab: Function) {}
render() {
this.tab = createElement('li', CLASS_TAB);
@ -282,7 +295,7 @@ class Tab {
return this.tab;
}
setActive(active) {
setActive(active: boolean) {
if (active) {
this.tab.classList.add(CLASS_ACTIVE_TAB);
this.tab.tabIndex = 0;
@ -295,11 +308,9 @@ class Tab {
}
class TabBody {
constructor(category, content, index) {
this.category = category;
this.content = content;
this.index = index;
}
constructor(private category: string, public content: HTMLElement, private index: number) {}
container: HTMLElement;
render() {
this.container = createElement('div', CLASS_TAB_BODY);
@ -313,7 +324,7 @@ class TabBody {
return this.container;
}
setActive(active) {
setActive(active: boolean) {
if (active) {
this.container.classList.add(CLASS_ACTIVE_TAB);
} else {

53
src/types.ts

@ -0,0 +1,53 @@
import { Placement } from '@popperjs/core';
export interface EmojiRecord {
n: string[];
e: string;
c: number;
ver: string;
v: { [key: string]: EmojiVariation };
}
export interface EmojiData {
categories: string[];
emojiData: EmojiRecord[];
}
export interface EmojiVariation {
k: string;
n: string;
e: string;
}
export interface RecentEmoji {
e: string;
n: string;
k: string;
}
export interface EmojiButtonOptions {
position?: Placement;
autoHide?: boolean;
autoFocusSearch?: boolean;
showPreview?: boolean;
showSearch?: boolean;
showRecents?: boolean;
showVariants?: boolean;
recentsCount?: number;
rootElement?: HTMLElement;
emojiVersion?: EmojiVersion;
i18n?: I18NStrings;
zIndex?: number;
}
export type EmojiVersion = '0.0' | '2.0' | '4.0' | '5.0' | '11.0' | '12.1';
export type I18NCategory = 'recents' | 'smileys' | 'animals' | 'food' | 'activities' | 'travel' | 'objects' | 'symbols' | 'flags';
export interface I18NStrings {
search: string;
categories: {
[key in I18NCategory]: string;
}
notFound: string;
}

8
src/util.js → src/util.ts

@ -1,4 +1,6 @@
export function createElement(tagName, className) {
import { EmojiRecord } from './types';
export function createElement(tagName: string, className?: string): HTMLElement {
const element = document.createElement(tagName);
if (className) {
@ -8,12 +10,12 @@ export function createElement(tagName, className) {
return element;
}
export function empty(element) {
export function empty(element: HTMLElement): void {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
export function getEmojiName(emoji) {
export function getEmojiName(emoji: EmojiRecord): string {
return typeof emoji.n === 'string' ? emoji.n : emoji.n[0];
}

0
src/variantPopup.test.js → src/variantPopup.test.ts

22
src/variantPopup.js → src/variantPopup.ts

@ -1,31 +1,33 @@
import { TinyEmitter as Emitter } from 'tiny-emitter';
import { Emoji } from './emoji';
import { createElement } from './util';
import { HIDE_VARIANT_POPUP } from './events';
import { times } from './icons';
import { EmojiRecord, EmojiButtonOptions } from './types';
const CLASS_OVERLAY = 'emoji-picker__variant-overlay';
const CLASS_POPUP = 'emoji-picker__variant-popup';
const CLASS_CLOSE_BUTTON = 'emoji-picker__variant-popup-close-button';
export class VariantPopup {
constructor(events, emoji, options) {
this.events = events;
this.emoji = emoji;
this.options = options;
}
private popup: HTMLElement;
private focusedEmojiIndex = 0;
constructor(private events: Emitter, private emoji: EmojiRecord, private options: EmojiButtonOptions) {}
getEmoji(index) {
return this.popup.querySelectorAll('.emoji-picker__emoji')[index];
}
setFocusedEmoji(newIndex) {
const currentFocusedEmoji = this.getEmoji(this.focusedEmojiIndex);
const currentFocusedEmoji = <HTMLElement> this.getEmoji(this.focusedEmojiIndex);
currentFocusedEmoji.tabIndex = -1;
this.focusedEmojiIndex = newIndex;
const newFocusedEmoji = this.getEmoji(this.focusedEmojiIndex);
const newFocusedEmoji = <HTMLElement> this.getEmoji(this.focusedEmojiIndex);
newFocusedEmoji.tabIndex = 0;
newFocusedEmoji.focus();
}
@ -34,10 +36,10 @@ export class VariantPopup {
this.popup = createElement('div', CLASS_POPUP);
const overlay = createElement('div', CLASS_OVERLAY);
overlay.addEventListener('click', event => {
overlay.addEventListener('click', (event: MouseEvent) => {
event.stopPropagation();
if (!this.popup.contains(event.target)) {
if (!this.popup.contains(<Node> event.target)) {
this.events.emit(HIDE_VARIANT_POPUP);
}
});
@ -57,7 +59,7 @@ export class VariantPopup {
);
});
const firstEmoji = this.popup.querySelector('.emoji-picker__emoji');
const firstEmoji = <HTMLElement> this.popup.querySelector('.emoji-picker__emoji');
this.focusedEmojiIndex = 0;
firstEmoji.tabIndex = 0;

67
tsconfig.json

@ -0,0 +1,67 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "es2015", /* 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…
Cancel
Save