From 5935cef219fdcbde17333c7aa12eb251870160e0 Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Fri, 12 Mar 2021 16:45:55 -0800 Subject: [PATCH 01/11] some manual virtual scrolling for each category --- src/lib/picker/category.component.ts | 140 +++++++++++++------- src/lib/picker/emoji-search.service.ts | 61 ++++----- src/lib/picker/ngx-emoji/emoji.component.ts | 32 ++--- src/lib/picker/ngx-emoji/emoji.service.ts | 33 ++--- src/lib/picker/picker.component.html | 8 +- src/lib/picker/picker.component.ts | 42 +++--- src/lib/picker/preview.component.ts | 106 ++++++++------- 7 files changed, 229 insertions(+), 193 deletions(-) diff --git a/src/lib/picker/category.component.ts b/src/lib/picker/category.component.ts index 83c909eb..73f90688 100644 --- a/src/lib/picker/category.component.ts +++ b/src/lib/picker/category.component.ts @@ -1,12 +1,15 @@ import { + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, + OnChanges, OnInit, Output, + SimpleChanges, ViewChild, } from '@angular/core'; @@ -16,43 +19,25 @@ import { EmojiFrequentlyService } from './emoji-frequently.service'; @Component({ selector: 'emoji-category', template: ` -
-
- - -
- - - - - -
-
+
+
+ + +
+ + + + +
+
+ +
+ +
+ {{ i18n.notfound }} +
- -
- {{ i18n.notfound }} -
-
- -
+ `, changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, }) -export class CategoryComponent implements OnInit { +export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { @Input() emojis?: any[] | null; @Input() hasStickyPosition = true; @Input() name = ''; @@ -94,6 +98,7 @@ export class CategoryComponent implements OnInit { @Input() emojiForceSize!: Emoji['forceSize']; @Input() emojiTooltip!: Emoji['tooltip']; @Input() emojiBackgroundImageFn?: Emoji['backgroundImageFn']; + @Input() emojiImageUrlFn?: Emoji['imageUrlFn']; @Input() emojiUseButton?: boolean; @Output() emojiOver: Emoji['emojiOver'] = new EventEmitter(); @Output() emojiLeave: Emoji['emojiLeave'] = new EventEmitter(); @@ -101,6 +106,7 @@ export class CategoryComponent implements OnInit { @ViewChild('container', { static: true }) container!: ElementRef; @ViewChild('label', { static: true }) label!: ElementRef; containerStyles: any = {}; + filteredEmojis?: any[] | null; labelStyles: any = {}; labelSpanStyles: any = {}; margin = 0; @@ -126,12 +132,36 @@ export class CategoryComponent implements OnInit { // this.labelSpanStyles = { position: 'absolute' }; } } + + ngOnChanges(changes: SimpleChanges) { + if (changes.emojis?.currentValue?.length !== changes.emojis?.previousValue?.length) { + this.ngAfterViewInit(); + } + } + + ngAfterViewInit() { + if (!this.emojis?.length) { + return; + } + + const parent = this.container.nativeElement.parentNode.parentNode; + const { width } = parent.getBoundingClientRect(); + + const rows = Math.ceil((this.emojis.length * (this.emojiSize + 12)) / width); + + this.containerStyles = { + ...this.containerStyles, + minHeight: `${rows * (this.emojiSize + 12) + 28}px`, + }; + + this.ref?.detectChanges(); + + this.handleScroll(this.container.nativeElement.parentNode.parentNode.scrollTop); + } + memoizeSize() { const parent = this.container.nativeElement.parentNode.parentNode; - const { - top, - height, - } = this.container.nativeElement.getBoundingClientRect(); + const { top, height } = this.container.nativeElement.getBoundingClientRect(); const parentTop = parent.getBoundingClientRect().top; const labelHeight = this.label.nativeElement.getBoundingClientRect().height; @@ -148,7 +178,15 @@ export class CategoryComponent implements OnInit { margin = margin < this.minMargin ? this.minMargin : margin; margin = margin > this.maxMargin ? this.maxMargin : margin; + const { top, height } = this.container.nativeElement.getBoundingClientRect(); + const parentHeight = this.container.nativeElement.parentNode.parentNode.clientHeight; + + if (parentHeight + 200 >= top && -height - 200 <= top) { + this.filteredEmojis = this.emojis; + } + if (margin === this.margin) { + this.ref.detectChanges(); return false; } @@ -157,12 +195,14 @@ export class CategoryComponent implements OnInit { } this.margin = margin; + this.ref.detectChanges(); return true; } getEmojis() { if (this.name === 'Recent') { - let frequentlyUsed = this.recent || this.frequently.get(this.perLine, this.totalFrequentLines); + let frequentlyUsed = + this.recent || this.frequently.get(this.perLine, this.totalFrequentLines); if (!frequentlyUsed || !frequentlyUsed.length) { frequentlyUsed = this.frequently.get(this.perLine, this.totalFrequentLines); } diff --git a/src/lib/picker/emoji-search.service.ts b/src/lib/picker/emoji-search.service.ts index be8b8aca..fa7a30cf 100644 --- a/src/lib/picker/emoji-search.service.ts +++ b/src/lib/picker/emoji-search.service.ts @@ -1,10 +1,6 @@ import { Injectable } from '@angular/core'; -import { - categories, - EmojiData, - EmojiService, -} from '@ctrl/ngx-emoji-mart/ngx-emoji'; +import { categories, EmojiData, EmojiService } from '@ctrl/ngx-emoji-mart/ngx-emoji'; import { intersect } from './utils'; @Injectable({ providedIn: 'root' }) @@ -24,13 +20,13 @@ export class EmojiSearch { const { shortNames, emoticons } = emojiData; const id = shortNames[0]; - emoticons.forEach(emoticon => { + for (const emoticon of emoticons) { if (this.emoticonsList[emoticon]) { - return; + continue; } this.emoticonsList[emoticon] = id; - }); + } this.emojisList[id] = this.emojiService.getSanitizedData(id); this.originalPool[id] = emojiData; @@ -38,14 +34,14 @@ export class EmojiSearch { } addCustomToPool(custom: any, pool: any) { - custom.forEach((emoji: any) => { + 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( @@ -79,28 +75,21 @@ export class EmojiSearch { if (include.length || exclude.length) { pool = {}; - categories.forEach(category => { - const isIncluded = - include && include.length - ? include.indexOf(category.id) > -1 - : true; - const isExcluded = - exclude && exclude.length - ? exclude.indexOf(category.id) > -1 - : false; + for (const category of categories || []) { + 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) { - return; + continue; } - category.emojis?.forEach( - emojiId => { - // 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; - } - ); - }); + 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 = @@ -142,7 +131,7 @@ export class EmojiSearch { emoji.name, emoji.id, emoji.keywords, - emoji.emoticons + emoji.emoticons, ); } const query = this.emojiSearch[id]; @@ -217,15 +206,19 @@ export class EmojiSearch { return; } - (Array.isArray(strings) ? strings : [strings]).forEach(str => { - (split ? str.split(/[-|_|\s]+/) : [str]).forEach(s => { + 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); diff --git a/src/lib/picker/ngx-emoji/emoji.component.ts b/src/lib/picker/ngx-emoji/emoji.component.ts index 0903c672..709908ad 100644 --- a/src/lib/picker/ngx-emoji/emoji.component.ts +++ b/src/lib/picker/ngx-emoji/emoji.component.ts @@ -4,7 +4,8 @@ import { EventEmitter, Input, OnChanges, - Output + Output, + SimpleChanges, } from '@angular/core'; import { EmojiData } from './data/data.interfaces'; @@ -16,7 +17,7 @@ export interface Emoji { forceSize: boolean; tooltip: boolean; skin: 1 | 2 | 3 | 4 | 5 | 6; - sheetSize: 16 | 20 | 32 | 64; + sheetSize: 16 | 20 | 32 | 64 | 72; sheetRows?: number; set: 'apple' | 'google' | 'twitter' | 'facebook' | ''; size: number; @@ -26,6 +27,7 @@ export interface Emoji { emojiOver: EventEmitter; emojiLeave: EventEmitter; emojiClick: EventEmitter; + imageUrlFn?: (emoji: EmojiData | null) => string; } export interface EmojiEvent { @@ -72,7 +74,7 @@ export interface EmojiEvent { `, changeDetection: ChangeDetectionStrategy.OnPush, - preserveWhitespaces: false + preserveWhitespaces: false, }) export class EmojiComponent implements OnChanges, Emoji { @Input() skin: Emoji['skin'] = 1; @@ -100,6 +102,7 @@ export class EmojiComponent implements OnChanges, Emoji { isVisible = true; // TODO: replace 4.0.3 w/ dynamic get verison from emoji-datasource in package.json @Input() backgroundImageFn: Emoji['backgroundImageFn'] = DEFAULT_BACKGROUNDFN; + @Input() imageUrlFn?: Emoji['imageUrlFn']; constructor(private emojiService: EmojiService) {} @@ -126,10 +129,7 @@ export class EmojiComponent implements OnChanges, Emoji { return (this.isVisible = false); } - this.label = [data.native] - .concat(data.shortNames) - .filter(Boolean) - .join(', '); + this.label = [data.native].concat(data.shortNames).filter(Boolean).join(', '); if (this.isNative && data.unified && data.native) { // hide older emoji before the split into gendered emoji @@ -145,23 +145,20 @@ export class EmojiComponent implements OnChanges, Emoji { this.style = { width: `${this.size}px`, height: `${this.size}px`, - display: 'inline-block' + display: 'inline-block', }; if (data.spriteUrl && this.sheetRows && this.sheetColumns) { this.style = { ...this.style, backgroundImage: `url(${data.spriteUrl})`, backgroundSize: `${100 * this.sheetColumns}% ${100 * this.sheetRows}%`, - backgroundPosition: this.emojiService.getSpritePosition( - data.sheet, - this.sheetColumns - ) + backgroundPosition: this.emojiService.getSpritePosition(data.sheet, this.sheetColumns), }; } else { this.style = { ...this.style, backgroundImage: `url(${data.imageUrl})`, - backgroundSize: 'contain' + backgroundSize: 'contain', }; } } else { @@ -180,7 +177,8 @@ export class EmojiComponent implements OnChanges, Emoji { this.sheetSize, this.sheetRows, this.backgroundImageFn, - this.sheetColumns + this.sheetColumns, + this.imageUrlFn?.(this.getData()), ); } } @@ -192,11 +190,7 @@ export class EmojiComponent implements OnChanges, Emoji { } getSanitizedData(): EmojiData { - return this.emojiService.getSanitizedData( - this.emoji, - this.skin, - this.set - ) as EmojiData; + return this.emojiService.getSanitizedData(this.emoji, this.skin, this.set) as EmojiData; } handleClick($event: Event) { diff --git a/src/lib/picker/ngx-emoji/emoji.service.ts b/src/lib/picker/ngx-emoji/emoji.service.ts index 988dc95e..830a4e7c 100644 --- a/src/lib/picker/ngx-emoji/emoji.service.ts +++ b/src/lib/picker/ngx-emoji/emoji.service.ts @@ -1,19 +1,13 @@ import { Injectable } from '@angular/core'; -import { - CompressedEmojiData, - EmojiData, - EmojiVariation, -} from './data/data.interfaces'; +import { CompressedEmojiData, EmojiData, EmojiVariation } from './data/data.interfaces'; import { emojis } from './data/emojis'; import { Emoji } from './emoji.component'; const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/; const SKINS = ['1F3FA', '1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF']; -export const DEFAULT_BACKGROUNDFN = ( - set: string, - sheetSize: number, -) => `https://unpkg.com/emoji-datasource-${set}@6.0.0/img/${set}/sheets-256/${sheetSize}.png`; +export const DEFAULT_BACKGROUNDFN = (set: string, sheetSize: number) => + `https://unpkg.com/emoji-datasource-${set}@6.0.0/img/${set}/sheets-256/${sheetSize}.png`; @Injectable({ providedIn: 'root' }) export class EmojiService { @@ -78,11 +72,7 @@ export class EmojiService { }); } - getData( - emoji: EmojiData | string, - skin?: Emoji['skin'], - set?: Emoji['set'], - ): EmojiData | null { + getData(emoji: EmojiData | string, skin?: Emoji['skin'], set?: Emoji['set']): EmojiData | null { let emojiData: any; if (typeof emoji === 'string') { @@ -144,14 +134,17 @@ export class EmojiService { sheetRows: Emoji['sheetRows'] = 57, backgroundImageFn: Emoji['backgroundImageFn'] = DEFAULT_BACKGROUNDFN, sheetColumns = 58, + url?: string, ) { + const hasImageUrl = !!url; + url = url || backgroundImageFn(set, sheetSize); return { width: `${size}px`, height: `${size}px`, display: 'inline-block', - 'background-image': `url(${backgroundImageFn(set, sheetSize)})`, - 'background-size': `${100 * sheetColumns}% ${100 * sheetRows}%`, - 'background-position': this.getSpritePosition(sheet, sheetColumns), + 'background-image': `url(${url})`, + 'background-size': hasImageUrl ? '100% 100%' : `${100 * sheetColumns}% ${100 * sheetRows}%`, + 'background-position': hasImageUrl ? undefined : this.getSpritePosition(sheet, sheetColumns), }; } @@ -174,11 +167,7 @@ export class EmojiService { return { ...emoji }; } - getSanitizedData( - emoji: string | EmojiData, - skin?: Emoji['skin'], - set?: Emoji['set'], - ) { + getSanitizedData(emoji: string | EmojiData, skin?: Emoji['skin'], set?: Emoji['set']) { return this.sanitize(this.getData(emoji, skin, set)); } } diff --git a/src/lib/picker/picker.component.html b/src/lib/picker/picker.component.html index cfb64ac4..ff255a8c 100644 --- a/src/lib/picker/picker.component.html +++ b/src/lib/picker/picker.component.html @@ -1,6 +1,8 @@ -
+ [ngStyle]="style" +>
diff --git a/src/lib/picker/picker.component.ts b/src/lib/picker/picker.component.ts index cd37de2d..0f609c0f 100644 --- a/src/lib/picker/picker.component.ts +++ b/src/lib/picker/picker.component.ts @@ -74,8 +74,7 @@ export class PickerComponent implements OnInit, OnDestroy { @Input() title = 'Emoji Mart™'; @Input() emoji = 'department_store'; @Input() darkMode = !!( - typeof matchMedia === 'function' && - matchMedia('(prefers-color-scheme: dark)').matches + typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches ); @Input() color = '#ae65c5'; @Input() hideObsolete = true; @@ -113,6 +112,7 @@ export class PickerComponent implements OnInit, OnDestroy { @ViewChildren(CategoryComponent) categoryRefs!: QueryList; scrollHeight = 0; clientHeight = 0; + clientWidth = 0; selected?: string; nextScroll?: string; scrollTop?: number; @@ -141,11 +141,10 @@ export class PickerComponent implements OnInit, OnDestroy { private scrollListener!: () => void; @Input() - backgroundImageFn: Emoji['backgroundImageFn'] = ( - set: string, - sheetSize: number, - ) => - `https://unpkg.com/emoji-datasource-${this.set}@6.0.0/img/${this.set}/sheets-256/${this.sheetSize}.png` + backgroundImageFn: Emoji['backgroundImageFn'] = (set: string, sheetSize: number) => + `https://unpkg.com/emoji-datasource-${this.set}@6.0.0/img/${this.set}/sheets-256/${this.sheetSize}.png`; + + @Input() imageUrlFn: Emoji['imageUrlFn']; constructor( private ngZone: NgZone, @@ -193,13 +192,9 @@ export class PickerComponent implements OnInit, OnDestroy { for (const category of allCategories) { const isIncluded = - this.include && this.include.length - ? this.include.indexOf(category.id) > -1 - : true; + this.include && this.include.length ? this.include.indexOf(category.id) > -1 : true; const isExcluded = - this.exclude && this.exclude.length - ? this.exclude.indexOf(category.id) > -1 - : false; + this.exclude && this.exclude.length ? this.exclude.indexOf(category.id) > -1 : false; if (!isIncluded || isExcluded) { continue; } @@ -255,7 +250,9 @@ export class PickerComponent implements OnInit, OnDestroy { // Need to be careful if small number of categories const categoriesToLoadFirst = Math.min(this.categories.length, 3); - this.setActiveCategories(this.activeCategories = this.categories.slice(0, categoriesToLoadFirst)); + this.setActiveCategories( + (this.activeCategories = this.categories.slice(0, categoriesToLoadFirst)), + ); // Trim last active category const lastActiveCategoryEmojis = this.categories[categoriesToLoadFirst - 1].emojis!.slice(); @@ -305,7 +302,7 @@ export class PickerComponent implements OnInit, OnDestroy { setActiveCategories(categoriesToMakeActive: Array) { if (this.showSingleCategory) { this.activeCategories = categoriesToMakeActive.filter( - x => (x.name === this.selected || x === this.SEARCH_CATEGORY) + x => x.name === this.selected || x === this.SEARCH_CATEGORY, ); } else { this.activeCategories = categoriesToMakeActive; @@ -318,6 +315,7 @@ export class PickerComponent implements OnInit, OnDestroy { const target = this.scrollRef.nativeElement; this.scrollHeight = target.scrollHeight; this.clientHeight = target.clientHeight; + this.clientWidth = target.clientWidth; } } handleAnchorClick($event: { category: EmojiCategory; index: number }) { @@ -343,13 +341,18 @@ export class PickerComponent implements OnInit, OnDestroy { } this.scrollRef.nativeElement.scrollTop = top; } - this.selected = $event.category.name; this.nextScroll = $event.category.name; + + // handle component scrolling to load emojis + for (const category of this.categories) { + const component = this.categoryRefs.find(({ id }) => id === category.id); + component?.handleScroll(this.scrollRef.nativeElement.scrollTop); + } } categoryTrack(index: number, item: any) { return item.id; } - handleScroll() { + handleScroll(noSelectionChange = false) { if (this.nextScroll) { this.selected = this.nextScroll; this.nextScroll = undefined; @@ -389,9 +392,11 @@ export class PickerComponent implements OnInit, OnDestroy { this.scrollTop = target.scrollTop; } // This will allow us to run the change detection only when the category changes. - if (activeCategory && activeCategory.name !== this.selected) { + if (!noSelectionChange && activeCategory && activeCategory.name !== this.selected) { this.selected = activeCategory.name; this.ref.detectChanges(); + } else if (noSelectionChange) { + this.ref.detectChanges(); } } handleSearch($emojis: any[] | null) { @@ -445,6 +450,7 @@ export class PickerComponent implements OnInit, OnDestroy { this.previewEmoji = $event.emoji; this.cancelAnimationFrame(); + this.ref?.detectChanges(); } handleEmojiLeave() { if (!this.showPreview || !this.previewRef) { diff --git a/src/lib/picker/preview.component.ts b/src/lib/picker/preview.component.ts index 0229dfff..ec9d792a 100644 --- a/src/lib/picker/preview.component.ts +++ b/src/lib/picker/preview.component.ts @@ -6,6 +6,7 @@ import { Input, OnChanges, Output, + SimpleChanges, } from '@angular/core'; import { Emoji, EmojiData, EmojiService } from '@ctrl/ngx-emoji-mart/ngx-emoji'; @@ -13,56 +14,62 @@ import { Emoji, EmojiData, EmojiService } from '@ctrl/ngx-emoji-mart/ngx-emoji'; @Component({ selector: 'emoji-preview', template: ` -
-
- -
- -
-
{{ emojiData.name }}
-
- - :{{ short_name }}: - +
+
+
-
- - {{ emoticon }} - + +
+
{{ emojiData.name }}
+
+ + :{{ short_name }}: + +
+
+ + {{ emoticon }} + +
-
-
-
- -
+
+
+ +
-
- {{ title }} -
+
+ {{ title }} +
-
- - +
+ + +
-
`, changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, @@ -78,20 +85,22 @@ export class PreviewComponent implements OnChanges { @Input() emojiSet?: Emoji['set']; @Input() emojiSheetSize?: Emoji['sheetSize']; @Input() emojiBackgroundImageFn?: Emoji['backgroundImageFn']; + @Input() emojiImageUrlFn?: Emoji['imageUrlFn']; @Output() skinChange = new EventEmitter(); emojiData: Partial = {}; listedEmoticons?: string[]; - constructor( - public ref: ChangeDetectorRef, - private emojiService: EmojiService, - ) {} + constructor(public ref: ChangeDetectorRef, private emojiService: EmojiService) {} ngOnChanges() { if (!this.emoji) { return; } - this.emojiData = this.emojiService.getData(this.emoji, this.emojiSkin, this.emojiSet) as EmojiData; + this.emojiData = this.emojiService.getData( + this.emoji, + this.emojiSkin, + this.emojiSet, + ) as EmojiData; const knownEmoticons: string[] = []; const listedEmoticons: string[] = []; const emoitcons = this.emojiData.emoticons || []; @@ -103,5 +112,6 @@ export class PreviewComponent implements OnChanges { listedEmoticons.push(emoticon); }); this.listedEmoticons = listedEmoticons; + this.ref?.detectChanges(); } } From 52376524b0358047dae58c2fb53546a9af5a735c Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Sat, 13 Mar 2021 12:03:22 -0800 Subject: [PATCH 02/11] alphabetize package dev dependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index bda3e491..c24f24c6 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,13 @@ "@angular-devkit/build-angular": "0.1101.2", "@angular/cli": "11.1.2", "@angular/common": "11.1.1", - "@angular/compiler-cli": "11.1.1", "@angular/compiler": "11.1.1", + "@angular/compiler-cli": "11.1.1", "@angular/core": "11.1.1", "@angular/forms": "11.1.1", "@angular/language-service": "11.1.1", - "@angular/platform-browser-dynamic": "11.1.1", "@angular/platform-browser": "11.1.1", + "@angular/platform-browser-dynamic": "11.1.1", "@ctrl/ngx-github-buttons": "6.1.0", "@types/inflection": "1.5.28", "@types/jasmine": "3.6.3", @@ -41,12 +41,12 @@ "emojilib": "2.4.0", "inflection": "1.12.0", "jasmine-core": "3.6.0", + "karma": "6.0.3", "karma-chrome-launcher": "3.1.0", "karma-coverage-istanbul-reporter": "3.0.3", - "karma-jasmine-html-reporter": "1.5.4", "karma-jasmine": "4.0.1", + "karma-jasmine-html-reporter": "1.5.4", "karma-mocha-reporter": "2.2.5", - "karma": "6.0.3", "ng-packagr": "11.1.2", "rxjs": "6.6.3", "stringify-object": "3.3.0", From f84b97d71fa8df58d06c3e13af7d8c18748fa07a Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Sat, 13 Mar 2021 12:03:48 -0800 Subject: [PATCH 03/11] optional virtualization --- src/lib/picker/category.component.ts | 45 +++++++++++++++++++++++----- src/lib/picker/picker.component.html | 1 + src/lib/picker/picker.component.ts | 1 + 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/lib/picker/category.component.ts b/src/lib/picker/category.component.ts index 73f90688..e395f74b 100644 --- a/src/lib/picker/category.component.ts +++ b/src/lib/picker/category.component.ts @@ -14,6 +14,7 @@ import { } from '@angular/core'; import { Emoji, EmojiService } from '@ctrl/ngx-emoji-mart/ngx-emoji'; +import { Observable, Subject } from 'rxjs'; import { EmojiFrequentlyService } from './emoji-frequently.service'; @Component({ @@ -33,7 +34,9 @@ import { EmojiFrequentlyService } from './emoji-frequently.service';
- +
- +
@@ -74,6 +77,28 @@ import { EmojiFrequentlyService } from './emoji-frequently.service';
+ + +
+ +
+
`, changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, @@ -90,6 +115,7 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { @Input() id: any; @Input() hideObsolete = true; @Input() notFoundEmoji?: string; + @Input() virtualize = false; @Input() emojiIsNative?: Emoji['isNative']; @Input() emojiSkin!: Emoji['skin']; @Input() emojiSize!: Emoji['size']; @@ -106,7 +132,8 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { @ViewChild('container', { static: true }) container!: ElementRef; @ViewChild('label', { static: true }) label!: ElementRef; containerStyles: any = {}; - filteredEmojis?: any[] | null; + private _filteredEmojis = new Subject(); + filteredEmojis$: Observable = this._filteredEmojis.asObservable(); labelStyles: any = {}; labelSpanStyles: any = {}; margin = 0; @@ -140,7 +167,7 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { } ngAfterViewInit() { - if (!this.emojis?.length) { + if (!this.virtualize || !this.emojis?.length) { return; } @@ -178,11 +205,13 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { margin = margin < this.minMargin ? this.minMargin : margin; margin = margin > this.maxMargin ? this.maxMargin : margin; - const { top, height } = this.container.nativeElement.getBoundingClientRect(); - const parentHeight = this.container.nativeElement.parentNode.parentNode.clientHeight; + if (this.virtualize) { + const { top, height } = this.container.nativeElement.getBoundingClientRect(); + const parentHeight = this.container.nativeElement.parentNode.parentNode.clientHeight; - if (parentHeight + 200 >= top && -height - 200 <= top) { - this.filteredEmojis = this.emojis; + if (parentHeight + 200 >= top && -height - 200 <= top) { + this._filteredEmojis.next(this.emojis); + } } if (margin === this.margin) { diff --git a/src/lib/picker/picker.component.html b/src/lib/picker/picker.component.html index ff255a8c..73d0c488 100644 --- a/src/lib/picker/picker.component.html +++ b/src/lib/picker/picker.component.html @@ -39,6 +39,7 @@ [notFoundEmoji]="notFoundEmoji" [custom]="category.id == RECENT_CATEGORY.id ? CUSTOM_CATEGORY.emojis : undefined" [recent]="category.id == RECENT_CATEGORY.id ? recent : undefined" + [virtualize]="virtualize" [emojiIsNative]="isNative" [emojiSkin]="skin" [emojiSize]="emojiSize" diff --git a/src/lib/picker/picker.component.ts b/src/lib/picker/picker.component.ts index 0f609c0f..a4e4a013 100644 --- a/src/lib/picker/picker.component.ts +++ b/src/lib/picker/picker.component.ts @@ -103,6 +103,7 @@ export class PickerComponent implements OnInit, OnDestroy { @Input() enableFrequentEmojiSort = false; @Input() enableSearch = true; @Input() showSingleCategory = false; + @Input() virtualize = false; @Output() emojiClick = new EventEmitter(); @Output() emojiSelect = new EventEmitter(); @Output() skinChange = new EventEmitter(); From 58bd93f67bfd9478066f22aff720ab0cfa6741f7 Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Sat, 13 Mar 2021 12:33:49 -0800 Subject: [PATCH 04/11] update readme with new input parameters --- README.md | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 490ccd88..d02cd602 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![CircleCI](https://badgen.net/circleci/github/scttcper/ngx-emoji-mart)](https://circleci.com/gh/scttcper/ngx-emoji-mart) [![codecov](https://img.shields.io/codecov/c/github/scttcper/ngx-emoji-mart.svg)](https://codecov.io/github/scttcper/ngx-emoji-mart) -**DEMO**: https://ngx-emoji-mart.vercel.app +**DEMO**: https://ngx-emoji-mart.vercel.app This project is a port of [emoji-mart](https://github.com/missive/emoji-mart) by missive @@ -67,9 +67,7 @@ use component - + @@ -92,9 +90,10 @@ use component | **totalFrequentLines** | `4` | number of lines of frequently used emojis | | **i18n** | [`{…}`](#i18n) | [An object](#i18n) containing localized strings | | **isNative** | `false` | Renders the native unicode emoji | -| **set** | `apple` | The emoji set: `'apple', 'google', 'twitter', 'facebook'` | +| **set** | `apple` | The emoji set: `'apple', 'google', 'twitter', 'facebook'` | | **sheetSize** | `64` | The emoji [sheet size](#sheet-sizes): `16, 20, 32, 64` | | **backgroundImageFn** | `((set, sheetSize) => …)` | A Fn that returns that image sheet to use for emojis. Useful for avoiding a request if you have the sheet locally. | +| **imageUrlFn** | `((emoji) => string)` | A Fn that returns the url used for the given emoji. Useful for fetching your own assets. | | **emojisToShowFilter** | `((emoji) => true)` | A Fn to choose whether an emoji should be displayed or not | | **showPreview** | `true` | Display preview section | | **enableSearch** | `true` | Display search bar | @@ -109,6 +108,7 @@ use component | **showSingleCategory** | | show only one category at a time to increase rendering performance | | **useButton** | `false` | Uses button elements for emoji instead of spans | | **enableFrequentEmojiSort** | `false` | Enables re-sorting of emoji on click | +| **virtualize** | `false` | Enables experimental virtualized rendering to render only emoji categories in view | #### I18n @@ -213,10 +213,10 @@ import { EmojiModule } from '@ctrl/ngx-emoji-mart/ngx-emoji'; ``` | Prop | Required | Default | Description | -| -------------------------------------------- | :------: | ------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| -------------------------------------------- | :------: | ------------------------- | ---------------------------------------------------------------------------------------------------------------- | --- | | **emoji** | ✓ | | Either a string or an `emoji` object | | **size** | ✓ | | The emoji width and height. | -| **isNative** | | `false` | Renders the native unicode emoji | +| **isNative** | | `false` | Renders the native unicode emoji | | **(emojiClick)** | | | Params: `{ emoji, $event }` | | **(emojiLeave)** | | | Params: `{ emoji, $event }` | | **(emojiOver)** | | | Params: `{ emoji, $event }` | @@ -225,9 +225,9 @@ import { EmojiModule } from '@ctrl/ngx-emoji-mart/ngx-emoji'; | **sheetSize** | | `64` | The emoji [sheet size](#sheet-sizes): `16, 20, 32, 64` | | **backgroundImageFn** | | `((set, sheetSize) => …)` | Fn that returns that image sheet to use for emojis. Useful for avoiding a request if you have the sheet locally. | | **skin** | | `1` | Skin color: `1, 2, 3, 4, 5, 6` | -| **tooltip** | | `false` | Show emoji short name when hovering (title) | | -| **hideObsolete** | | `false` | Hides ex: "cop" emoji in favor of female and male emoji | | -| **useButton** | | `false` | Uses button element instead of span | | +| **tooltip** | | `false` | Show emoji short name when hovering (title) | | +| **hideObsolete** | | `false` | Hides ex: "cop" emoji in favor of female and male emoji | | +| **useButton** | | `false` | Uses button element instead of span | | #### Unsupported emojis fallback @@ -236,17 +236,11 @@ Certain sets don’t support all emojis (i.e. Facebook doesn't support `:shrug:` To have the component render `:shrug:` you would need to: ```ts -emojiFallback = (emoji: any, props: any) => - emoji ? `:${emoji.shortNames[0]}:` : props.emoji; +emojiFallback = (emoji: any, props: any) => (emoji ? `:${emoji.shortNames[0]}:` : props.emoji); ``` ```html - + ``` ## Custom emojis @@ -269,8 +263,7 @@ const customEmojis = [ text: '', emoticons: [], keywords: ['test', 'flag'], - spriteUrl: - 'https://unpkg.com/emoji-datasource-twitter@6.0.0/img/twitter/sheets-256/64.png', + spriteUrl: 'https://unpkg.com/emoji-datasource-twitter@6.0.0/img/twitter/sheets-256/64.png', sheet_x: 1, sheet_y: 1, size: 64, @@ -292,7 +285,7 @@ The `Picker` doesn’t have to be mounted for you to take advantage of the advan import { EmojiSearch } from '@ctrl/ngx-emoji-mart'; class ex { constructor(private emojiSearch: EmojiSearch) { - this.emojiSearch.search('christmas').map((o) => o.native); + this.emojiSearch.search('christmas').map(o => o.native); // => [🎄, 🎅🏼, 🔔, 🎁, ⛄️, ❄️] } } From 14a61784610499db54a4d45d9cd860421515bcbf Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Sat, 13 Mar 2021 13:03:02 -0800 Subject: [PATCH 05/11] fix linting errors --- src/lib/picker/category.component.ts | 6 +++--- src/lib/picker/picker.component.ts | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib/picker/category.component.ts b/src/lib/picker/category.component.ts index e395f74b..78546cd3 100644 --- a/src/lib/picker/category.component.ts +++ b/src/lib/picker/category.component.ts @@ -132,8 +132,8 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { @ViewChild('container', { static: true }) container!: ElementRef; @ViewChild('label', { static: true }) label!: ElementRef; containerStyles: any = {}; - private _filteredEmojis = new Subject(); - filteredEmojis$: Observable = this._filteredEmojis.asObservable(); + private filteredEmojisSubject = new Subject(); + filteredEmojis$: Observable = this.filteredEmojisSubject.asObservable(); labelStyles: any = {}; labelSpanStyles: any = {}; margin = 0; @@ -210,7 +210,7 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { const parentHeight = this.container.nativeElement.parentNode.parentNode.clientHeight; if (parentHeight + 200 >= top && -height - 200 <= top) { - this._filteredEmojis.next(this.emojis); + this.filteredEmojisSubject.next(this.emojis); } } diff --git a/src/lib/picker/picker.component.ts b/src/lib/picker/picker.component.ts index a4e4a013..76f00fa1 100644 --- a/src/lib/picker/picker.component.ts +++ b/src/lib/picker/picker.component.ts @@ -94,6 +94,7 @@ export class PickerComponent implements OnInit, OnDestroy { @Input() autoFocus = false; @Input() custom: any[] = []; @Input() hideRecent = true; + @Input() imageUrlFn: Emoji['imageUrlFn']; @Input() include?: string[]; @Input() exclude?: string[]; @Input() notFoundEmoji = 'sleuth_or_spy'; @@ -143,9 +144,7 @@ export class PickerComponent implements OnInit, OnDestroy { @Input() backgroundImageFn: Emoji['backgroundImageFn'] = (set: string, sheetSize: number) => - `https://unpkg.com/emoji-datasource-${this.set}@6.0.0/img/${this.set}/sheets-256/${this.sheetSize}.png`; - - @Input() imageUrlFn: Emoji['imageUrlFn']; + `https://unpkg.com/emoji-datasource-${this.set}@6.0.0/img/${this.set}/sheets-256/${this.sheetSize}.png` constructor( private ngZone: NgZone, @@ -346,8 +345,8 @@ export class PickerComponent implements OnInit, OnDestroy { // handle component scrolling to load emojis for (const category of this.categories) { - const component = this.categoryRefs.find(({ id }) => id === category.id); - component?.handleScroll(this.scrollRef.nativeElement.scrollTop); + const componentToScroll = this.categoryRefs.find(({ id }) => id === category.id); + componentToScroll?.handleScroll(this.scrollRef.nativeElement.scrollTop); } } categoryTrack(index: number, item: any) { From 1eebc7c4eacfd34a56b9afaa5a32f2b18d49d4b3 Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Wed, 17 Mar 2021 13:38:05 -0700 Subject: [PATCH 06/11] pass emoji use button prop --- src/lib/picker/category.component.ts | 5 +++-- src/lib/picker/picker.component.html | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/picker/category.component.ts b/src/lib/picker/category.component.ts index 78546cd3..9ef81282 100644 --- a/src/lib/picker/category.component.ts +++ b/src/lib/picker/category.component.ts @@ -10,13 +10,13 @@ import { OnInit, Output, SimpleChanges, - ViewChild, + ViewChild } from '@angular/core'; - import { Emoji, EmojiService } from '@ctrl/ngx-emoji-mart/ngx-emoji'; import { Observable, Subject } from 'rxjs'; import { EmojiFrequentlyService } from './emoji-frequently.service'; + @Component({ selector: 'emoji-category', template: ` @@ -50,6 +50,7 @@ import { EmojiFrequentlyService } from './emoji-frequently.service'; [backgroundImageFn]="emojiBackgroundImageFn" [imageUrlFn]="emojiImageUrlFn" [hideObsolete]="hideObsolete" + [useButton]="emojiUseButton" (emojiOver)="emojiOver.emit($event)" (emojiLeave)="emojiLeave.emit($event)" (emojiClick)="emojiClick.emit($event)" diff --git a/src/lib/picker/picker.component.html b/src/lib/picker/picker.component.html index 73d0c488..47073371 100644 --- a/src/lib/picker/picker.component.html +++ b/src/lib/picker/picker.component.html @@ -49,7 +49,7 @@ [emojiTooltip]="emojiTooltip" [emojiBackgroundImageFn]="backgroundImageFn" [emojiImageUrlFn]="imageUrlFn" - [emojiUseButton]="false" + [emojiUseButton]="useButton" (emojiOver)="handleEmojiOver($event)" (emojiLeave)="handleEmojiLeave()" (emojiClick)="handleEmojiClick($event)" From 679430569fee735ee1b036707be4670e72480253 Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Thu, 18 Mar 2021 11:04:46 -0700 Subject: [PATCH 07/11] add virtualizeOffset input --- README.md | 1 + src/lib/picker/category.component.ts | 3 ++- src/lib/picker/picker.component.html | 1 + src/lib/picker/picker.component.ts | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d02cd602..5132b32b 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ use component | **useButton** | `false` | Uses button elements for emoji instead of spans | | **enableFrequentEmojiSort** | `false` | Enables re-sorting of emoji on click | | **virtualize** | `false` | Enables experimental virtualized rendering to render only emoji categories in view | +| **virtualizeOffset** | `0` | use with virtualize option to add or subtract the amount of pixels used to determine whether or not render the category | #### I18n diff --git a/src/lib/picker/category.component.ts b/src/lib/picker/category.component.ts index 9ef81282..7f1a6acd 100644 --- a/src/lib/picker/category.component.ts +++ b/src/lib/picker/category.component.ts @@ -117,6 +117,7 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { @Input() hideObsolete = true; @Input() notFoundEmoji?: string; @Input() virtualize = false; + @Input() virtualizeOffset = 0; @Input() emojiIsNative?: Emoji['isNative']; @Input() emojiSkin!: Emoji['skin']; @Input() emojiSize!: Emoji['size']; @@ -210,7 +211,7 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { const { top, height } = this.container.nativeElement.getBoundingClientRect(); const parentHeight = this.container.nativeElement.parentNode.parentNode.clientHeight; - if (parentHeight + 200 >= top && -height - 200 <= top) { + if (parentHeight + (parentHeight + this.virtualizeOffset) >= top && -height - (parentHeight + this.virtualizeOffset) <= top) { this.filteredEmojisSubject.next(this.emojis); } } diff --git a/src/lib/picker/picker.component.html b/src/lib/picker/picker.component.html index 47073371..85ee4652 100644 --- a/src/lib/picker/picker.component.html +++ b/src/lib/picker/picker.component.html @@ -40,6 +40,7 @@ [custom]="category.id == RECENT_CATEGORY.id ? CUSTOM_CATEGORY.emojis : undefined" [recent]="category.id == RECENT_CATEGORY.id ? recent : undefined" [virtualize]="virtualize" + [virtualizeOffset]="virtualizeOffset" [emojiIsNative]="isNative" [emojiSkin]="skin" [emojiSize]="emojiSize" diff --git a/src/lib/picker/picker.component.ts b/src/lib/picker/picker.component.ts index 76f00fa1..b30f86f4 100644 --- a/src/lib/picker/picker.component.ts +++ b/src/lib/picker/picker.component.ts @@ -105,6 +105,7 @@ export class PickerComponent implements OnInit, OnDestroy { @Input() enableSearch = true; @Input() showSingleCategory = false; @Input() virtualize = false; + @Input() virtualizeOffset = 0; @Output() emojiClick = new EventEmitter(); @Output() emojiSelect = new EventEmitter(); @Output() skinChange = new EventEmitter(); From cf224d98a76b83cd56b192d9549f2f883100b324 Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Thu, 25 Mar 2021 21:01:50 -0700 Subject: [PATCH 08/11] fix perRow and height measuring, filter out invisible emojis --- src/lib/picker/category.component.ts | 75 ++++++++++++++++++---------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/src/lib/picker/category.component.ts b/src/lib/picker/category.component.ts index 7f1a6acd..21a00078 100644 --- a/src/lib/picker/category.component.ts +++ b/src/lib/picker/category.component.ts @@ -1,3 +1,4 @@ +import { Emoji, EmojiService } from '@ctrl/ngx-emoji-mart/ngx-emoji'; import { AfterViewInit, ChangeDetectionStrategy, @@ -12,7 +13,6 @@ import { SimpleChanges, ViewChild } from '@angular/core'; -import { Emoji, EmojiService } from '@ctrl/ngx-emoji-mart/ngx-emoji'; import { Observable, Subject } from 'rxjs'; import { EmojiFrequentlyService } from './emoji-frequently.service'; @@ -35,26 +35,28 @@ import { EmojiFrequentlyService } from './emoji-frequently.service';
- +
+ +
@@ -80,7 +82,7 @@ import { EmojiFrequentlyService } from './emoji-frequently.service'; -
+
= top && -height - (parentHeight + this.virtualizeOffset) <= top) { this.filteredEmojisSubject.next(this.emojis); + } else { + this.filteredEmojisSubject.next([]); } } @@ -269,4 +275,23 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { trackById(index: number, item: any) { return item; } + + private filterEmojis(): any[] | null | undefined { + if (!this.emojis) { + return this.emojis; + } + + 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; + } } From 764deee1d6a0dc1e7324c85027b6b9dbe33ca72d Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Thu, 25 Mar 2021 21:02:40 -0700 Subject: [PATCH 09/11] remove unused SimpleChanges --- src/lib/picker/ngx-emoji/emoji.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/picker/ngx-emoji/emoji.component.ts b/src/lib/picker/ngx-emoji/emoji.component.ts index 709908ad..c4336527 100644 --- a/src/lib/picker/ngx-emoji/emoji.component.ts +++ b/src/lib/picker/ngx-emoji/emoji.component.ts @@ -5,7 +5,6 @@ import { Input, OnChanges, Output, - SimpleChanges, } from '@angular/core'; import { EmojiData } from './data/data.interfaces'; From b1b311d925ae7877c29acfc3b1090f233f81154a Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Thu, 25 Mar 2021 21:03:03 -0700 Subject: [PATCH 10/11] remove unused SimpleChanges --- src/lib/picker/preview.component.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/picker/preview.component.ts b/src/lib/picker/preview.component.ts index ec9d792a..eb139004 100644 --- a/src/lib/picker/preview.component.ts +++ b/src/lib/picker/preview.component.ts @@ -1,3 +1,4 @@ +import { Emoji, EmojiData, EmojiService } from '@ctrl/ngx-emoji-mart/ngx-emoji'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -5,11 +6,9 @@ import { EventEmitter, Input, OnChanges, - Output, - SimpleChanges, + Output } from '@angular/core'; -import { Emoji, EmojiData, EmojiService } from '@ctrl/ngx-emoji-mart/ngx-emoji'; @Component({ selector: 'emoji-preview', From f14d4f3d2978cad10688a3ae47002a1e6cfa6313 Mon Sep 17 00:00:00 2001 From: Andrew Giangrant Date: Thu, 25 Mar 2021 21:06:00 -0700 Subject: [PATCH 11/11] don't filter emojis if virtualize is false --- src/lib/picker/category.component.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/lib/picker/category.component.ts b/src/lib/picker/category.component.ts index 21a00078..26f7ecb6 100644 --- a/src/lib/picker/category.component.ts +++ b/src/lib/picker/category.component.ts @@ -167,7 +167,6 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { ngOnChanges(changes: SimpleChanges) { if (changes.emojis?.currentValue?.length !== changes.emojis?.previousValue?.length) { - this.emojis = this.filterEmojis(); this.ngAfterViewInit(); } } @@ -177,6 +176,8 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { return; } + this.emojis = this.filterEmojis(); + const { width } = this.container.nativeElement.getBoundingClientRect(); const perRow = Math.floor(width / (this.emojiSize + 12)); @@ -276,11 +277,7 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit { return item; } - private filterEmojis(): any[] | null | undefined { - if (!this.emojis) { - return this.emojis; - } - + private filterEmojis(): any[] { const newEmojis = []; for (const emoji of this.emojis || []) { if (!emoji) {