@ctrl/ngx-emoji-mart
Version:
Customizable Slack-like emoji picker for Angular
1,265 lines (1,240 loc) • 86.1 kB
JavaScript
import * as i2 from '@angular/common';
import { CommonModule, isPlatformBrowser } from '@angular/common';
import * as i0 from '@angular/core';
import { EventEmitter, Component, ChangeDetectionStrategy, Input, Output, PLATFORM_ID, Injectable, Inject, ViewChild, ViewChildren, NgModule } from '@angular/core';
import * as i1 from '@ctrl/ngx-emoji-mart/ngx-emoji';
import { EmojiComponent, categories as categories$1 } from '@ctrl/ngx-emoji-mart/ngx-emoji';
import { Subject, fromEvent, takeUntil } from 'rxjs';
import * as i2$1 from '@angular/forms';
import { FormsModule } from '@angular/forms';
class AnchorsComponent {
categories = [];
color;
selected;
i18n;
icons = {};
anchorClick = new EventEmitter();
trackByFn(idx, cat) {
return cat.id;
}
handleClick($event, index) {
this.anchorClick.emit({
category: this.categories[index],
index,
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: AnchorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.2", type: AnchorsComponent, isStandalone: true, selector: "emoji-mart-anchors", inputs: { categories: "categories", color: "color", selected: "selected", i18n: "i18n", icons: "icons" }, outputs: { anchorClick: "anchorClick" }, ngImport: i0, template: `
<div class="emoji-mart-anchors">
<ng-template
ngFor
let-category
[ngForOf]="categories"
let-idx="index"
[ngForTrackBy]="trackByFn"
>
<span
*ngIf="category.anchor !== false"
[attr.title]="i18n.categories[category.id]"
(click)="this.handleClick($event, idx)"
class="emoji-mart-anchor"
[class.emoji-mart-anchor-selected]="category.name === selected"
[style.color]="category.name === selected ? color : null"
>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path [attr.d]="icons[category.id]" />
</svg>
</div>
<span class="emoji-mart-anchor-bar" [style.background-color]="color"></span>
</span>
</ng-template>
</div>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: AnchorsComponent, decorators: [{
type: Component,
args: [{
selector: 'emoji-mart-anchors',
template: `
<div class="emoji-mart-anchors">
<ng-template
ngFor
let-category
[ngForOf]="categories"
let-idx="index"
[ngForTrackBy]="trackByFn"
>
<span
*ngIf="category.anchor !== false"
[attr.title]="i18n.categories[category.id]"
(click)="this.handleClick($event, idx)"
class="emoji-mart-anchor"
[class.emoji-mart-anchor-selected]="category.name === selected"
[style.color]="category.name === selected ? color : null"
>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path [attr.d]="icons[category.id]" />
</svg>
</div>
<span class="emoji-mart-anchor-bar" [style.background-color]="color"></span>
</span>
</ng-template>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
preserveWhitespaces: false,
standalone: true,
imports: [CommonModule],
}]
}], propDecorators: { categories: [{
type: Input
}], color: [{
type: Input
}], selected: [{
type: Input
}], i18n: [{
type: Input
}], icons: [{
type: Input
}], anchorClick: [{
type: Output
}] } });
class EmojiFrequentlyService {
platformId;
NAMESPACE = 'emoji-mart';
frequently = null;
defaults = {};
initialized = false;
DEFAULTS = [
'+1',
'grinning',
'kissing_heart',
'heart_eyes',
'laughing',
'stuck_out_tongue_winking_eye',
'sweat_smile',
'joy',
'scream',
'disappointed',
'unamused',
'weary',
'sob',
'sunglasses',
'heart',
'poop',
];
constructor(platformId) {
this.platformId = platformId;
}
init() {
this.frequently = JSON.parse((isPlatformBrowser(this.platformId) &&
localStorage.getItem(`${this.NAMESPACE}.frequently`)) ||
'null');
this.initialized = true;
}
add(emoji) {
if (!this.initialized) {
this.init();
}
if (!this.frequently) {
this.frequently = this.defaults;
}
if (!this.frequently[emoji.id]) {
this.frequently[emoji.id] = 0;
}
this.frequently[emoji.id] += 1;
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem(`${this.NAMESPACE}.last`, emoji.id);
localStorage.setItem(`${this.NAMESPACE}.frequently`, JSON.stringify(this.frequently));
}
}
get(perLine, totalLines) {
if (!this.initialized) {
this.init();
}
if (this.frequently === null) {
this.defaults = {};
const result = [];
for (let i = 0; i < perLine; i++) {
this.defaults[this.DEFAULTS[i]] = perLine - i;
result.push(this.DEFAULTS[i]);
}
return result;
}
const quantity = perLine * totalLines;
const frequentlyKeys = Object.keys(this.frequently);
const sorted = frequentlyKeys
.sort((a, b) => this.frequently[a] - this.frequently[b])
.reverse();
const sliced = sorted.slice(0, quantity);
const last = isPlatformBrowser(this.platformId) && localStorage.getItem(`${this.NAMESPACE}.last`);
if (last && !sliced.includes(last)) {
sliced.pop();
sliced.push(last);
}
return sliced;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: EmojiFrequentlyService, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: EmojiFrequentlyService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: EmojiFrequentlyService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: function () { return [{ type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }]; } });
class CategoryComponent {
ref;
emojiService;
frequently;
emojis = null;
hasStickyPosition = true;
name = '';
perLine = 9;
totalFrequentLines = 4;
recent = [];
custom = [];
i18n;
id;
hideObsolete = true;
notFoundEmoji;
virtualize = false;
virtualizeOffset = 0;
emojiIsNative;
emojiSkin;
emojiSize;
emojiSet;
emojiSheetSize;
emojiForceSize;
emojiTooltip;
emojiBackgroundImageFn;
emojiImageUrlFn;
emojiUseButton;
/**
* Note: the suffix is added explicitly so we know the event is dispatched outside of the Angular zone.
*/
emojiOverOutsideAngular = new EventEmitter();
emojiLeaveOutsideAngular = new EventEmitter();
emojiClickOutsideAngular = new EventEmitter();
container;
label;
containerStyles = {};
emojisToDisplay = [];
filteredEmojisSubject = new Subject();
filteredEmojis$ = this.filteredEmojisSubject.asObservable();
labelStyles = {};
labelSpanStyles = {};
margin = 0;
minMargin = 0;
maxMargin = 0;
top = 0;
rows = 0;
constructor(ref, emojiService, frequently) {
this.ref = ref;
this.emojiService = emojiService;
this.frequently = frequently;
}
ngOnInit() {
this.updateRecentEmojis();
this.emojisToDisplay = this.filterEmojis();
if (this.noEmojiToDisplay) {
this.containerStyles = { display: 'none' };
}
if (!this.hasStickyPosition) {
this.labelStyles = { height: 28 };
// this.labelSpanStyles = { position: 'absolute' };
}
}
ngOnChanges(changes) {
if (changes.emojis?.currentValue?.length !== changes.emojis?.previousValue?.length) {
this.emojisToDisplay = this.filterEmojis();
this.ngAfterViewInit();
}
}
ngAfterViewInit() {
if (!this.virtualize) {
return;
}
const { width } = this.container.nativeElement.getBoundingClientRect();
const perRow = Math.floor(width / (this.emojiSize + 12));
this.rows = Math.ceil(this.emojisToDisplay.length / perRow);
this.containerStyles = {
...this.containerStyles,
minHeight: `${this.rows * (this.emojiSize + 12) + 28}px`,
};
this.ref.detectChanges();
this.handleScroll(this.container.nativeElement.parentNode.parentNode.scrollTop);
}
get noEmojiToDisplay() {
return this.emojisToDisplay.length === 0;
}
memoizeSize() {
const parent = this.container.nativeElement.parentNode.parentNode;
const { top, height } = this.container.nativeElement.getBoundingClientRect();
const parentTop = parent.getBoundingClientRect().top;
const labelHeight = this.label.nativeElement.getBoundingClientRect().height;
this.top = top - parentTop + parent.scrollTop;
if (height === 0) {
this.maxMargin = 0;
}
else {
this.maxMargin = height - labelHeight;
}
}
handleScroll(scrollTop) {
let margin = scrollTop - this.top;
margin = margin < this.minMargin ? this.minMargin : margin;
margin = margin > this.maxMargin ? this.maxMargin : margin;
if (this.virtualize) {
const { top, height } = this.container.nativeElement.getBoundingClientRect();
const parentHeight = this.container.nativeElement.parentNode.parentNode.clientHeight;
if (parentHeight + (parentHeight + this.virtualizeOffset) >= top &&
-height - (parentHeight + this.virtualizeOffset) <= top) {
this.filteredEmojisSubject.next(this.emojisToDisplay);
}
else {
this.filteredEmojisSubject.next([]);
}
}
if (margin === this.margin) {
this.ref.detectChanges();
return false;
}
if (!this.hasStickyPosition) {
this.label.nativeElement.style.top = `${margin}px`;
}
this.margin = margin;
this.ref.detectChanges();
return true;
}
updateRecentEmojis() {
if (this.name !== 'Recent') {
return;
}
let frequentlyUsed = this.recent || this.frequently.get(this.perLine, this.totalFrequentLines);
if (!frequentlyUsed || !frequentlyUsed.length) {
frequentlyUsed = this.frequently.get(this.perLine, this.totalFrequentLines);
}
if (!frequentlyUsed.length) {
return;
}
this.emojis = frequentlyUsed
.map(id => {
const emoji = this.custom.filter((e) => e.id === id)[0];
if (emoji) {
return emoji;
}
return id;
})
.filter(id => !!this.emojiService.getData(id));
}
updateDisplay(display) {
this.containerStyles.display = display;
this.updateRecentEmojis();
this.ref.detectChanges();
}
trackById(index, item) {
return item;
}
filterEmojis() {
const newEmojis = [];
for (const emoji of this.emojis || []) {
if (!emoji) {
continue;
}
const data = this.emojiService.getData(emoji);
if (!data || (data.obsoletedBy && this.hideObsolete) || (!data.unified && !data.custom)) {
continue;
}
newEmojis.push(emoji);
}
return newEmojis;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: CategoryComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i1.EmojiService }, { token: EmojiFrequentlyService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.2", type: CategoryComponent, isStandalone: true, selector: "emoji-category", inputs: { emojis: "emojis", hasStickyPosition: "hasStickyPosition", name: "name", perLine: "perLine", totalFrequentLines: "totalFrequentLines", recent: "recent", custom: "custom", i18n: "i18n", id: "id", hideObsolete: "hideObsolete", notFoundEmoji: "notFoundEmoji", virtualize: "virtualize", virtualizeOffset: "virtualizeOffset", emojiIsNative: "emojiIsNative", emojiSkin: "emojiSkin", emojiSize: "emojiSize", emojiSet: "emojiSet", emojiSheetSize: "emojiSheetSize", emojiForceSize: "emojiForceSize", emojiTooltip: "emojiTooltip", emojiBackgroundImageFn: "emojiBackgroundImageFn", emojiImageUrlFn: "emojiImageUrlFn", emojiUseButton: "emojiUseButton" }, outputs: { emojiOverOutsideAngular: "emojiOverOutsideAngular", emojiLeaveOutsideAngular: "emojiLeaveOutsideAngular", emojiClickOutsideAngular: "emojiClickOutsideAngular" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, static: true }, { propertyName: "label", first: true, predicate: ["label"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
<section
#container
class="emoji-mart-category"
[attr.aria-label]="i18n.categories[id]"
[class.emoji-mart-no-results]="noEmojiToDisplay"
[ngStyle]="containerStyles"
>
<div class="emoji-mart-category-label" [ngStyle]="labelStyles" [attr.data-name]="name">
<!-- already labeled by the section aria-label -->
<span #label [ngStyle]="labelSpanStyles" aria-hidden="true">
{{ i18n.categories[id] }}
</span>
</div>
<div *ngIf="virtualize; else normalRenderTemplate">
<div *ngIf="filteredEmojis$ | async as filteredEmojis">
<ngx-emoji
*ngFor="let emoji of filteredEmojis; trackBy: trackById"
[emoji]="emoji"
[size]="emojiSize"
[skin]="emojiSkin"
[isNative]="emojiIsNative"
[set]="emojiSet"
[sheetSize]="emojiSheetSize"
[forceSize]="emojiForceSize"
[tooltip]="emojiTooltip"
[backgroundImageFn]="emojiBackgroundImageFn"
[imageUrlFn]="emojiImageUrlFn"
[hideObsolete]="hideObsolete"
[useButton]="emojiUseButton"
(emojiOverOutsideAngular)="emojiOverOutsideAngular.emit($event)"
(emojiLeaveOutsideAngular)="emojiLeaveOutsideAngular.emit($event)"
(emojiClickOutsideAngular)="emojiClickOutsideAngular.emit($event)"
></ngx-emoji>
</div>
</div>
<div *ngIf="noEmojiToDisplay">
<div>
<ngx-emoji
[emoji]="notFoundEmoji"
[size]="38"
[skin]="emojiSkin"
[isNative]="emojiIsNative"
[set]="emojiSet"
[sheetSize]="emojiSheetSize"
[forceSize]="emojiForceSize"
[tooltip]="emojiTooltip"
[backgroundImageFn]="emojiBackgroundImageFn"
[useButton]="emojiUseButton"
></ngx-emoji>
</div>
<div class="emoji-mart-no-results-label">
{{ i18n.notfound }}
</div>
</div>
</section>
<ng-template #normalRenderTemplate>
<ngx-emoji
*ngFor="let emoji of emojisToDisplay; trackBy: trackById"
[emoji]="emoji"
[size]="emojiSize"
[skin]="emojiSkin"
[isNative]="emojiIsNative"
[set]="emojiSet"
[sheetSize]="emojiSheetSize"
[forceSize]="emojiForceSize"
[tooltip]="emojiTooltip"
[backgroundImageFn]="emojiBackgroundImageFn"
[imageUrlFn]="emojiImageUrlFn"
[hideObsolete]="hideObsolete"
[useButton]="emojiUseButton"
(emojiOverOutsideAngular)="emojiOverOutsideAngular.emit($event)"
(emojiLeaveOutsideAngular)="emojiLeaveOutsideAngular.emit($event)"
(emojiClickOutsideAngular)="emojiClickOutsideAngular.emit($event)"
></ngx-emoji>
</ng-template>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i2.AsyncPipe, name: "async" }, { kind: "component", type: EmojiComponent, selector: "ngx-emoji", inputs: ["skin", "set", "sheetSize", "isNative", "forceSize", "tooltip", "size", "emoji", "fallback", "hideObsolete", "sheetRows", "sheetColumns", "useButton", "backgroundImageFn", "imageUrlFn"], outputs: ["emojiOver", "emojiOverOutsideAngular", "emojiLeave", "emojiLeaveOutsideAngular", "emojiClick", "emojiClickOutsideAngular"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: CategoryComponent, decorators: [{
type: Component,
args: [{
selector: 'emoji-category',
template: `
<section
#container
class="emoji-mart-category"
[attr.aria-label]="i18n.categories[id]"
[class.emoji-mart-no-results]="noEmojiToDisplay"
[ngStyle]="containerStyles"
>
<div class="emoji-mart-category-label" [ngStyle]="labelStyles" [attr.data-name]="name">
<!-- already labeled by the section aria-label -->
<span #label [ngStyle]="labelSpanStyles" aria-hidden="true">
{{ i18n.categories[id] }}
</span>
</div>
<div *ngIf="virtualize; else normalRenderTemplate">
<div *ngIf="filteredEmojis$ | async as filteredEmojis">
<ngx-emoji
*ngFor="let emoji of filteredEmojis; trackBy: trackById"
[emoji]="emoji"
[size]="emojiSize"
[skin]="emojiSkin"
[isNative]="emojiIsNative"
[set]="emojiSet"
[sheetSize]="emojiSheetSize"
[forceSize]="emojiForceSize"
[tooltip]="emojiTooltip"
[backgroundImageFn]="emojiBackgroundImageFn"
[imageUrlFn]="emojiImageUrlFn"
[hideObsolete]="hideObsolete"
[useButton]="emojiUseButton"
(emojiOverOutsideAngular)="emojiOverOutsideAngular.emit($event)"
(emojiLeaveOutsideAngular)="emojiLeaveOutsideAngular.emit($event)"
(emojiClickOutsideAngular)="emojiClickOutsideAngular.emit($event)"
></ngx-emoji>
</div>
</div>
<div *ngIf="noEmojiToDisplay">
<div>
<ngx-emoji
[emoji]="notFoundEmoji"
[size]="38"
[skin]="emojiSkin"
[isNative]="emojiIsNative"
[set]="emojiSet"
[sheetSize]="emojiSheetSize"
[forceSize]="emojiForceSize"
[tooltip]="emojiTooltip"
[backgroundImageFn]="emojiBackgroundImageFn"
[useButton]="emojiUseButton"
></ngx-emoji>
</div>
<div class="emoji-mart-no-results-label">
{{ i18n.notfound }}
</div>
</div>
</section>
<ng-template #normalRenderTemplate>
<ngx-emoji
*ngFor="let emoji of emojisToDisplay; trackBy: trackById"
[emoji]="emoji"
[size]="emojiSize"
[skin]="emojiSkin"
[isNative]="emojiIsNative"
[set]="emojiSet"
[sheetSize]="emojiSheetSize"
[forceSize]="emojiForceSize"
[tooltip]="emojiTooltip"
[backgroundImageFn]="emojiBackgroundImageFn"
[imageUrlFn]="emojiImageUrlFn"
[hideObsolete]="hideObsolete"
[useButton]="emojiUseButton"
(emojiOverOutsideAngular)="emojiOverOutsideAngular.emit($event)"
(emojiLeaveOutsideAngular)="emojiLeaveOutsideAngular.emit($event)"
(emojiClickOutsideAngular)="emojiClickOutsideAngular.emit($event)"
></ngx-emoji>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
preserveWhitespaces: false,
standalone: true,
imports: [CommonModule, EmojiComponent],
}]
}], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }, { type: i1.EmojiService }, { type: EmojiFrequentlyService }]; }, propDecorators: { emojis: [{
type: Input
}], hasStickyPosition: [{
type: Input
}], name: [{
type: Input
}], perLine: [{
type: Input
}], totalFrequentLines: [{
type: Input
}], recent: [{
type: Input
}], custom: [{
type: Input
}], i18n: [{
type: Input
}], id: [{
type: Input
}], hideObsolete: [{
type: Input
}], notFoundEmoji: [{
type: Input
}], virtualize: [{
type: Input
}], virtualizeOffset: [{
type: Input
}], emojiIsNative: [{
type: Input
}], emojiSkin: [{
type: Input
}], emojiSize: [{
type: Input
}], emojiSet: [{
type: Input
}], emojiSheetSize: [{
type: Input
}], emojiForceSize: [{
type: Input
}], emojiTooltip: [{
type: Input
}], emojiBackgroundImageFn: [{
type: Input
}], emojiImageUrlFn: [{
type: Input
}], emojiUseButton: [{
type: Input
}], emojiOverOutsideAngular: [{
type: Output
}], emojiLeaveOutsideAngular: [{
type: Output
}], emojiClickOutsideAngular: [{
type: Output
}], container: [{
type: ViewChild,
args: ['container', { static: true }]
}], label: [{
type: ViewChild,
args: ['label', { static: true }]
}] } });
function uniq(arr) {
return arr.reduce((acc, item) => {
if (!acc.includes(item)) {
acc.push(item);
}
return acc;
}, []);
}
function intersect(a, b) {
const uniqA = uniq(a);
const uniqB = uniq(b);
return uniqA.filter((item) => uniqB.indexOf(item) >= 0);
}
// https://github.com/sonicdoe/measure-scrollbar
function measureScrollbar() {
if (typeof document === 'undefined') {
return 0;
}
const div = document.createElement('div');
div.style.width = '100px';
div.style.height = '100px';
div.style.overflow = 'scroll';
div.style.position = 'absolute';
div.style.top = '-9999px';
document.body.appendChild(div);
const scrollbarWidth = div.offsetWidth - div.clientWidth;
document.body.removeChild(div);
return scrollbarWidth;
}
class EmojiSearch {
emojiService;
originalPool = {};
index = {};
emojisList = {};
emoticonsList = {};
emojiSearch = {};
constructor(emojiService) {
this.emojiService = emojiService;
for (const emojiData of this.emojiService.emojis) {
const { shortNames, emoticons } = emojiData;
const id = shortNames[0];
for (const emoticon of emoticons) {
if (this.emoticonsList[emoticon]) {
continue;
}
this.emoticonsList[emoticon] = id;
}
this.emojisList[id] = this.emojiService.getSanitizedData(id);
this.originalPool[id] = emojiData;
}
}
addCustomToPool(custom, pool) {
for (const emoji of custom) {
const emojiId = emoji.id || emoji.shortNames[0];
if (emojiId && !pool[emojiId]) {
pool[emojiId] = this.emojiService.getData(emoji);
this.emojisList[emojiId] = this.emojiService.getSanitizedData(emoji);
}
}
}
search(value, emojisToShowFilter, maxResults = 75, include = [], exclude = [], custom = []) {
this.addCustomToPool(custom, this.originalPool);
let results;
let pool = this.originalPool;
if (value.length) {
if (value === '-' || value === '-1') {
return [this.emojisList['-1']];
}
if (value === '+' || value === '+1') {
return [this.emojisList['+1']];
}
let values = value.toLowerCase().split(/[\s|,|\-|_]+/);
let allResults = [];
if (values.length > 2) {
values = [values[0], values[1]];
}
if (include.length || exclude.length) {
pool = {};
for (const category of categories$1 || []) {
const isIncluded = include && include.length ? include.indexOf(category.id) > -1 : true;
const isExcluded = exclude && exclude.length ? exclude.indexOf(category.id) > -1 : false;
if (!isIncluded || isExcluded) {
continue;
}
for (const emojiId of category.emojis || []) {
// Need to make sure that pool gets keyed
// with the correct id, which is why we call emojiService.getData below
const emoji = this.emojiService.getData(emojiId);
pool[emoji?.id ?? ''] = emoji;
}
}
if (custom.length) {
const customIsIncluded = include && include.length ? include.indexOf('custom') > -1 : true;
const customIsExcluded = exclude && exclude.length ? exclude.indexOf('custom') > -1 : false;
if (customIsIncluded && !customIsExcluded) {
this.addCustomToPool(custom, pool);
}
}
}
allResults = values
.map(v => {
let aPool = pool;
let aIndex = this.index;
let length = 0;
for (let charIndex = 0; charIndex < v.length; charIndex++) {
const char = v[charIndex];
length++;
if (!aIndex[char]) {
aIndex[char] = {};
}
aIndex = aIndex[char];
if (!aIndex.results) {
const scores = {};
aIndex.results = [];
aIndex.pool = {};
for (const id of Object.keys(aPool)) {
const emoji = aPool[id];
if (!this.emojiSearch[id]) {
this.emojiSearch[id] = this.buildSearch(emoji.short_names, emoji.name, emoji.id, emoji.keywords, emoji.emoticons);
}
const query = this.emojiSearch[id];
const sub = v.substr(0, length);
const subIndex = query.indexOf(sub);
if (subIndex !== -1) {
let score = subIndex + 1;
if (sub === id) {
score = 0;
}
aIndex.results.push(this.emojisList[id]);
aIndex.pool[id] = emoji;
scores[id] = score;
}
}
aIndex.results.sort((a, b) => {
const aScore = scores[a.id];
const bScore = scores[b.id];
return aScore - bScore;
});
}
aPool = aIndex.pool;
}
return aIndex.results;
})
.filter(a => a);
if (allResults.length > 1) {
results = intersect.apply(null, allResults);
}
else if (allResults.length) {
results = allResults[0];
}
else {
results = [];
}
}
if (results) {
if (emojisToShowFilter) {
results = results.filter((result) => {
if (result && result.id) {
return emojisToShowFilter(this.emojiService.names[result.id]);
}
return false;
});
}
if (results && results.length > maxResults) {
results = results.slice(0, maxResults);
}
}
return results || null;
}
buildSearch(shortNames, name, id, keywords, emoticons) {
const search = [];
const addToSearch = (strings, split) => {
if (!strings) {
return;
}
const arr = Array.isArray(strings) ? strings : [strings];
for (const str of arr) {
const substrings = split ? str.split(/[-|_|\s]+/) : [str];
for (let s of substrings) {
s = s.toLowerCase();
if (!search.includes(s)) {
search.push(s);
}
}
}
};
addToSearch(shortNames, true);
addToSearch(name, true);
addToSearch(id, true);
addToSearch(keywords, true);
addToSearch(emoticons, false);
return search.join(',');
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: EmojiSearch, deps: [{ token: i1.EmojiService }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: EmojiSearch, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: EmojiSearch, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: function () { return [{ type: i1.EmojiService }]; } });
class SkinComponent {
/** currently selected skin */
skin;
i18n;
changeSkin = new EventEmitter();
opened = false;
skinTones = [1, 2, 3, 4, 5, 6];
toggleOpen() {
this.opened = !this.opened;
}
isSelected(skinTone) {
return skinTone === this.skin;
}
isVisible(skinTone) {
return this.opened || this.isSelected(skinTone);
}
pressed(skinTone) {
return this.opened ? !!this.isSelected(skinTone) : '';
}
tabIndex(skinTone) {
return this.isVisible(skinTone) ? '0' : '';
}
expanded(skinTone) {
return this.isSelected(skinTone) ? this.opened : '';
}
handleClick(skin) {
if (!this.opened) {
this.opened = true;
return;
}
this.opened = false;
if (skin !== this.skin) {
this.changeSkin.emit(skin);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: SkinComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.2", type: SkinComponent, isStandalone: true, selector: "emoji-skins", inputs: { skin: "skin", i18n: "i18n" }, outputs: { changeSkin: "changeSkin" }, ngImport: i0, template: `
<section class="emoji-mart-skin-swatches" [class.opened]="opened">
<span
*ngFor="let skinTone of skinTones"
class="emoji-mart-skin-swatch"
[class.selected]="skinTone === skin"
>
<span
(click)="handleClick(skinTone)"
(keyup.enter)="handleClick(skinTone)"
(keyup.space)="handleClick(skinTone)"
class="emoji-mart-skin emoji-mart-skin-tone-{{ skinTone }}"
role="button"
[tabIndex]="tabIndex(skinTone)"
[attr.aria-hidden]="!isVisible(skinTone)"
[attr.aria-pressed]="pressed(skinTone)"
[attr.aria-haspopup]="!!isSelected(skinTone)"
[attr.aria-expanded]="expanded(skinTone)"
[attr.aria-label]="i18n.skintones[skinTone]"
[attr.title]="i18n.skintones[skinTone]"
></span>
</span>
</section>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: SkinComponent, decorators: [{
type: Component,
args: [{
selector: 'emoji-skins',
template: `
<section class="emoji-mart-skin-swatches" [class.opened]="opened">
<span
*ngFor="let skinTone of skinTones"
class="emoji-mart-skin-swatch"
[class.selected]="skinTone === skin"
>
<span
(click)="handleClick(skinTone)"
(keyup.enter)="handleClick(skinTone)"
(keyup.space)="handleClick(skinTone)"
class="emoji-mart-skin emoji-mart-skin-tone-{{ skinTone }}"
role="button"
[tabIndex]="tabIndex(skinTone)"
[attr.aria-hidden]="!isVisible(skinTone)"
[attr.aria-pressed]="pressed(skinTone)"
[attr.aria-haspopup]="!!isSelected(skinTone)"
[attr.aria-expanded]="expanded(skinTone)"
[attr.aria-label]="i18n.skintones[skinTone]"
[attr.title]="i18n.skintones[skinTone]"
></span>
</span>
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
preserveWhitespaces: false,
standalone: true,
imports: [CommonModule],
}]
}], propDecorators: { skin: [{
type: Input
}], i18n: [{
type: Input
}], changeSkin: [{
type: Output
}] } });
class PreviewComponent {
ref;
emojiService;
title;
emoji;
idleEmoji;
i18n;
emojiIsNative;
emojiSkin;
emojiSize;
emojiSet;
emojiSheetSize;
emojiBackgroundImageFn;
emojiImageUrlFn;
skinChange = new EventEmitter();
emojiData = {};
listedEmoticons;
constructor(ref, emojiService) {
this.ref = ref;
this.emojiService = emojiService;
}
ngOnChanges() {
if (!this.emoji) {
return;
}
this.emojiData = this.emojiService.getData(this.emoji, this.emojiSkin, this.emojiSet);
const knownEmoticons = [];
const listedEmoticons = [];
const emoitcons = this.emojiData.emoticons || [];
emoitcons.forEach((emoticon) => {
if (knownEmoticons.indexOf(emoticon.toLowerCase()) >= 0) {
return;
}
knownEmoticons.push(emoticon.toLowerCase());
listedEmoticons.push(emoticon);
});
this.listedEmoticons = listedEmoticons;
this.ref?.detectChanges();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: PreviewComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i1.EmojiService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.2", type: PreviewComponent, isStandalone: true, selector: "emoji-preview", inputs: { title: "title", emoji: "emoji", idleEmoji: "idleEmoji", i18n: "i18n", emojiIsNative: "emojiIsNative", emojiSkin: "emojiSkin", emojiSize: "emojiSize", emojiSet: "emojiSet", emojiSheetSize: "emojiSheetSize", emojiBackgroundImageFn: "emojiBackgroundImageFn", emojiImageUrlFn: "emojiImageUrlFn" }, outputs: { skinChange: "skinChange" }, usesOnChanges: true, ngImport: i0, template: `
<div class="emoji-mart-preview" *ngIf="emoji && emojiData">
<div class="emoji-mart-preview-emoji">
<ngx-emoji
[emoji]="emoji"
[size]="38"
[isNative]="emojiIsNative"
[skin]="emojiSkin"
[size]="emojiSize"
[set]="emojiSet"
[sheetSize]="emojiSheetSize"
[backgroundImageFn]="emojiBackgroundImageFn"
[imageUrlFn]="emojiImageUrlFn"
></ngx-emoji>
</div>
<div class="emoji-mart-preview-data">
<div class="emoji-mart-preview-name">{{ emojiData.name }}</div>
<div class="emoji-mart-preview-shortname">
<span
class="emoji-mart-preview-shortname"
*ngFor="let short_name of emojiData.shortNames"
>
:{{ short_name }}:
</span>
</div>
<div class="emoji-mart-preview-emoticons">
<span class="emoji-mart-preview-emoticon" *ngFor="let emoticon of listedEmoticons">
{{ emoticon }}
</span>
</div>
</div>
</div>
<div class="emoji-mart-preview" [hidden]="emoji">
<div class="emoji-mart-preview-emoji">
<ngx-emoji
*ngIf="idleEmoji && idleEmoji.length"
[isNative]="emojiIsNative"
[skin]="emojiSkin"
[set]="emojiSet"
[emoji]="idleEmoji"
[backgroundImageFn]="emojiBackgroundImageFn"
[size]="38"
[imageUrlFn]="emojiImageUrlFn"
></ngx-emoji>
</div>
<div class="emoji-mart-preview-data">
<span class="emoji-mart-title-label">{{ title }}</span>
</div>
<div class="emoji-mart-preview-skins">
<emoji-skins
[skin]="emojiSkin"
(changeSkin)="skinChange.emit($event)"
[i18n]="i18n"
></emoji-skins>
</div>
</div>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: EmojiComponent, selector: "ngx-emoji", inputs: ["skin", "set", "sheetSize", "isNative", "forceSize", "tooltip", "size", "emoji", "fallback", "hideObsolete", "sheetRows", "sheetColumns", "useButton", "backgroundImageFn", "imageUrlFn"], outputs: ["emojiOver", "emojiOverOutsideAngular", "emojiLeave", "emojiLeaveOutsideAngular", "emojiClick", "emojiClickOutsideAngular"] }, { kind: "component", type: SkinComponent, selector: "emoji-skins", inputs: ["skin", "i18n"], outputs: ["changeSkin"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: PreviewComponent, decorators: [{
type: Component,
args: [{
selector: 'emoji-preview',
template: `
<div class="emoji-mart-preview" *ngIf="emoji && emojiData">
<div class="emoji-mart-preview-emoji">
<ngx-emoji
[emoji]="emoji"
[size]="38"
[isNative]="emojiIsNative"
[skin]="emojiSkin"
[size]="emojiSize"
[set]="emojiSet"
[sheetSize]="emojiSheetSize"
[backgroundImageFn]="emojiBackgroundImageFn"
[imageUrlFn]="emojiImageUrlFn"
></ngx-emoji>
</div>
<div class="emoji-mart-preview-data">
<div class="emoji-mart-preview-name">{{ emojiData.name }}</div>
<div class="emoji-mart-preview-shortname">
<span
class="emoji-mart-preview-shortname"
*ngFor="let short_name of emojiData.shortNames"
>
:{{ short_name }}:
</span>
</div>
<div class="emoji-mart-preview-emoticons">
<span class="emoji-mart-preview-emoticon" *ngFor="let emoticon of listedEmoticons">
{{ emoticon }}
</span>
</div>
</div>
</div>
<div class="emoji-mart-preview" [hidden]="emoji">
<div class="emoji-mart-preview-emoji">
<ngx-emoji
*ngIf="idleEmoji && idleEmoji.length"
[isNative]="emojiIsNative"
[skin]="emojiSkin"
[set]="emojiSet"
[emoji]="idleEmoji"
[backgroundImageFn]="emojiBackgroundImageFn"
[size]="38"
[imageUrlFn]="emojiImageUrlFn"
></ngx-emoji>
</div>
<div class="emoji-mart-preview-data">
<span class="emoji-mart-title-label">{{ title }}</span>
</div>
<div class="emoji-mart-preview-skins">
<emoji-skins
[skin]="emojiSkin"
(changeSkin)="skinChange.emit($event)"
[i18n]="i18n"
></emoji-skins>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
preserveWhitespaces: false,
standalone: true,
imports: [CommonModule, EmojiComponent, SkinComponent],
}]
}], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }, { type: i1.EmojiService }]; }, propDecorators: { title: [{
type: Input
}], emoji: [{
type: Input
}], idleEmoji: [{
type: Input
}], i18n: [{
type: Input
}], emojiIsNative: [{
type: Input
}], emojiSkin: [{
type: Input
}], emojiSize: [{
type: Input
}], emojiSet: [{
type: Input
}], emojiSheetSize: [{
type: Input
}], emojiBackgroundImageFn: [{
type: Input
}], emojiImageUrlFn: [{
type: Input
}], skinChange: [{
type: Output
}] } });
let id = 0;
class SearchComponent {
ngZone;
emojiSearch;
maxResults = 75;
autoFocus = false;
i18n;
include = [];
exclude = [];
custom = [];
icons;
emojisToShowFilter;
searchResults = new EventEmitter();
enterKeyOutsideAngular = new EventEmitter();
inputRef;
isSearching = false;
icon;
query = '';
inputId = `emoji-mart-search-${++id}`;
destroy$ = new Subject();
constructor(ngZone, emojiSearch) {
this.ngZone = ngZone;
this.emojiSearch = emojiSearch;
}
ngOnInit() {
this.icon = this.icons.search;
this.setupKeyupListener();
}
ngAfterViewInit() {
if (this.autoFocus) {
this.inputRef.nativeElement.focus();
}
}
ngOnDestroy() {
this.destroy$.next();
}
clear() {
this.query = '';
this.handleSearch('');
this.inputRef.nativeElement.focus();
}
handleSearch(value) {
if (value === '') {
this.icon = this.icons.search;
this.isSearching = false;
}
else {
this.icon = this.icons.delete;
this.isSearching = true;
}
const emojis = this.emojiSearch.search(this.query, this.emojisToShowFilter, this.maxResults, this.include, this.exclude, this.custom);
this.searchResults.emit(emojis);
}
handleChange() {
this.handleSearch(this.query);
}
setupKeyupListener() {
this.ngZone.runOutsideAngular(() => fromEvent(this.inputRef.nativeElement, 'keyup')
.pipe(takeUntil(this.destroy$))
.subscribe($event => {
if (!this.query || $event.key !== 'Enter') {
return;
}
this.enterKeyOutsideAngular.emit($event);
$event.preventDefault();
}));
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: SearchComponent, deps: [{ token: i0.NgZone }, { token: EmojiSearch }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.2", type: SearchComponent, isStandalone: true, selector: "emoji-search", inputs: { maxResults: "maxResults", autoFocus: "autoFocus", i18n: "i18n", include: "include", exclude: "exclude", custom: "custom", icons: "icons", emojisToShowFilter: "emojisToShowFilter" }, outputs: { searchResults: "searchResults", enterKeyOutsideAngular: "enterKeyOutsideAngular" }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputRef"], descendants: true, static: true }], ngImport: i0, template: `
<div class="emoji-mart-search">
<input
[id]="inputId"
#inputRef
type="search"
[placeholder]="i18n.search"
[autofocus]="autoFocus"
[(ngModel)]="query"
(ngModelChange)="handleChange()"
/>
<!--
Use a <label> in addition to the placeholder for accessibility, but place it off-screen
http://www.maxability.co.in/2016/01/placeholder-attribute-and-why-it-is-not-accessible/
-->
<label class="emoji-mart-sr-only" [htmlFor]="inputId">
{{ i18n.search }}
</label>
<button
type="button"
class="emoji-mart-search-icon"
(click)="clear()"
(keyup.enter)="clear()"
[disabled]="!isSearching"
[attr.aria-label]="i18n.clear"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
width="13"
height="13"
opacity="0.5"
>
<path [attr.d]="icon" />
</svg>
</button>
</div>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.2", ngImport: i0, type: SearchComponent, decorators: [{
type: Component,
args: [{
selector: 'emoji-search',
template: `
<div class="emoji-mart-search">
<input
[id]="inputId"
#inputRef
type="search"
[placeholder]="i18n.search"
[autofocus]="autoFocus"
[(ngModel)]="query"
(ngModelChange)="handleChange()"
/>
<!--
Use a <label> in addition to the placeholder for accessibility, but place it off-screen
http://www.maxability.co.in/2016/01/placeholder-attribute-and-why-it-is-not-accessible/
-->
<label class="emoji-mart-sr-only" [htmlFor]="inputId">
{{ i18n.search }}
</label>
<button
type="button"
class="emoji-mart-search-icon"
(click)="clear()"
(keyup.enter)="clear()"
[disabled]="!isSearching"
[attr.aria-label]="i18n.clear"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
width="13"
height="13"
opacity="0.5"
>
<path [attr.d]="icon" />
</svg>
</button>
</div>
`,
preserveWhitespaces: false,
standalone: true,
imports: [FormsModule],
}]
}], ctorParameters: function () { return [{ type: i0.NgZone }, { type: EmojiSearch }]; }, propDecorators: { maxResults: [{
type: Input
}], autoFocus: [{
type: Input
}], i18n: [{
type: Input
}], include: [{
type: Input
}], exclude: [{
type: Input
}], custom: [{
type: Input
}], icons: [{
type: Input
}], emojisToShowFilter: [{
type: Input
}], searchResults: [{
type: Output