diff --git a/docs/docs/02 - usage/15 - recent-emojis.md b/docs/docs/02 - usage/15 - recent-emojis.md new file mode 100644 index 00000000..c98e3fd5 --- /dev/null +++ b/docs/docs/02 - usage/15 - recent-emojis.md @@ -0,0 +1,19 @@ +# Recent emojis + +PicMo will keep track of your most recently used emojis in the "Recently Used" category. As emojis are selected, they will be added to the "Recently Used" category. + +## Recents providers + +### Built-in web storage providers + +By default, recent emoji data is kept in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). This is managed by the [`LocalStorageProvider`](../api/picmo/classes/local-storage-provider) class. + +PicMo also includes a [`SessionStorageProvider`](../api/picmo/classes/session-storage-provider) class that uses [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) instead. + +A provider is selected by passing a `recentsProvider` option to the [`createPicker`](../api/picmo/functions/create-picker) function. The value should be an *instance* of the provider class. + +### Creating a custom recents provider + +You can create a custom provider to store recent emojis in some other way. To do this, you need to create a class that extends from the [`RecentsProvider`](../api/picmo/classes/recents-provider) class and implement the three methods required (`clear`, `getRecents`, and `addOrUpdateRecent`). + +An instance of this class should be passed to the `recentsProvider` option of the [`createPicker`](../api/picmo/functions/create-picker) function. diff --git a/docs/docs/api/picmo/classes/emoji-picker.md b/docs/docs/api/picmo/classes/emoji-picker.md index 78ca7271..b8101fc6 100644 --- a/docs/docs/api/picmo/classes/emoji-picker.md +++ b/docs/docs/api/picmo/classes/emoji-picker.md @@ -22,6 +22,13 @@ Adds an event listener to the picker. - `listener`: The function to call when this event is emitted. - **Type**: `function` +### `clearRecents` + +``` +clearRecents(): void +``` + +Removes all recent emojis from the picker, as managed by the recents provider. ### `destroy` diff --git a/docs/docs/api/picmo/classes/local-storage-provider.md b/docs/docs/api/picmo/classes/local-storage-provider.md new file mode 100644 index 00000000..5822f153 --- /dev/null +++ b/docs/docs/api/picmo/classes/local-storage-provider.md @@ -0,0 +1,3 @@ +# `LocalStorageProvider` + +A [`RecentsProvider`](./recents-provider) that uses [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store the recent emojis. diff --git a/docs/docs/api/picmo/classes/native-renderer.md b/docs/docs/api/picmo/classes/native-renderer.md index 965a157f..ca6340e5 100644 --- a/docs/docs/api/picmo/classes/native-renderer.md +++ b/docs/docs/api/picmo/classes/native-renderer.md @@ -3,7 +3,7 @@ Renders emojis using the native operating system font glyphs. ``` -import { NativeRenderer } from 'picmo/renderers/native'; +import { NativeRenderer } from 'picmo'; ```
diff --git a/docs/docs/api/picmo/classes/recents-provider.md b/docs/docs/api/picmo/classes/recents-provider.md new file mode 100644 index 00000000..c9ed723a --- /dev/null +++ b/docs/docs/api/picmo/classes/recents-provider.md @@ -0,0 +1,35 @@ +# `RecentsProvider` + +Abstract base class that provides a storage mechanism for remembering recently selected emojis. + +To create a custom recents provider, create a subclass of `RecentsProvider`. + +``` +import { RecentsProvider } from 'picmo'; +``` + +## Methods + +### `clear` + +``` +clear(): void +``` + +Removes all stored recent emojis. + +### `getRecents` + ++getRecents(maxCount: number): EmojiRecord[] ++ +Gets the current set of recent emojis. The returned list will be truncated to satisfy the `maxCount` parameter. + +### `addOrUpdateRecent` + ++addOrUpdateRecent(emoji: EmojiRecord, maxCount: number): void ++ +Adds a new recent emoji to the list, or updates an existing one if it already exists. diff --git a/docs/docs/api/picmo/classes/session-storage-provider.md b/docs/docs/api/picmo/classes/session-storage-provider.md new file mode 100644 index 00000000..113345eb --- /dev/null +++ b/docs/docs/api/picmo/classes/session-storage-provider.md @@ -0,0 +1,3 @@ +# `SessionStorageProvider` + +A [`RecentsProvider`](./recents-provider) that uses [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) to store the recent emojis. diff --git a/docs/docs/api/picmo/types/emoji-record.md b/docs/docs/api/picmo/types/emoji-record.md new file mode 100644 index 00000000..2e0e36a0 --- /dev/null +++ b/docs/docs/api/picmo/types/emoji-record.md @@ -0,0 +1,75 @@ +# `EmojiRecord` + +Describes a single emoji inside the picker. + +This is _not_ what is emitted when an emoji is selected; for that, see [`EmojiSelection`](./emoji-selection). Rather, it is used internally by the picker as well as the recents provider. + +## Properties + +### `custom` + +- **Type**: `boolean` | `undefined` + +Whether or not this is a custom emoji. + +### `data` + +- **Type**: `any` | `undefined` + +Arbitrary data associated with an emoji. Only applies to custom emojis. + +### `emoji` + +- **Type**: `string` + +The native emoji string containing the Unicode sequenece for the emoji. For custom emojis, this is a unique identifier for the custom emoji. + +### `hexcode` + +- **Type**: `string` | `undefined` + +The hex codes for the emoji, separated by hyphens, e.g. `'1F3CB-1F3FB-200D-2642-FE0F'`. + +Only applies to non-custom emojis. + +### `label` + +- **Type**: `string` + +The display name for the emoji. + +### `order` + +- **Type**: `number` | `undefined` + +The order of the emoji within its category. + +Only applies to non-custom emojis; custom emojis are ordered depending on their order in the `custom` array. + +### `skins` + +- **Type**: `EmojiRecord[]` | `undefined` + +Any variants of the emoji, typically for different skin tones, though sometimes they can be for other attributes such as hair color. + +### `tags` + +- **Type**: `string[]` | `undefined` + +An array of tags for this emoji, if any. + +### `url` + +- **Type**: `string` | `undefined` + +For custom emojis, the URL of the image to use for the emoji. + +Only applies to custom emojis. + +### `version` + +- **Type**: `number` | `undefined` + +For non-custom emojis, specifies the version of the Emoji specification that the emoji was added in. + +Only applies to non-custom emojis. diff --git a/docs/docs/api/picmo/types/picker-options.md b/docs/docs/api/picmo/types/picker-options.md index 9ee57185..28444bd3 100644 --- a/docs/docs/api/picmo/types/picker-options.md +++ b/docs/docs/api/picmo/types/picker-options.md @@ -120,6 +120,13 @@ The maximum number of recent emojis to cache locally. The messages data to use for the picker, containing category names. If not specified, the data will be downloaded from the CDN when the database is created. +### `recentsProvider` + +- **Type**: [`RecentsProvider`](./recents-provider) subclass +- **Default**: Instance of [`LocalStorageProvider`](./local-storage-provider) + +The provider to use for storing recent emojis. + ### `renderer` - **Type**: [`Renderer`](../classes/renderer) subclass instance diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index ed45bcbf..bda6c0f2 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -56,7 +56,7 @@ const config = { type: 'doc', docId: 'getting-started/overview', position: 'right', - label: 'Docs' + label: 'Guide' }, { type: 'doc', @@ -83,7 +83,7 @@ const config = { style: 'dark', links: [ { - title: 'Docs', + title: 'Guide', items: [ { label: 'Getting Started', diff --git a/packages/picmo/src/index.ts b/packages/picmo/src/index.ts index 16ebdeec..cdce1e81 100644 --- a/packages/picmo/src/index.ts +++ b/packages/picmo/src/index.ts @@ -1,24 +1,29 @@ import { Locale, MessagesDataset, Emoji } from 'emojibase'; import { initDatabase as initDatabaseInternal } from './data/emojiData'; -export { createPicker } from './createPicker'; -export { Renderer } from './renderers/renderer'; -export { deleteDatabase } from './data/emojiData'; -export { clear as deleteRecents } from './recents'; -export { EmojiPicker } from './views/EmojiPicker'; export * from './util'; export * from './focusTrap'; export * from './options'; export * from './events'; -export { ExternalEvent, ExternalEventKey } from './ExternalEvents'; export * from './types'; export * from './themes'; -import { DataStoreFactory } from './data/DataStore'; + +export { createPicker } from './createPicker'; +export { EmojiPicker } from './views/EmojiPicker'; +export { ExternalEvent, ExternalEventKey } from './ExternalEvents'; + +export { Renderer } from './renderers/renderer'; export { NativeRenderer } from './renderers/native'; + export { default as en } from './i18n/lang-en'; + +export { deleteDatabase } from './data/emojiData'; +import { DataStoreFactory } from './data/DataStore'; export { IndexedDbStoreFactory } from './data/IndexedDbStore'; export { InMemoryStoreFactory } from './data/InMemoryStore'; +export * from './recents/index'; + export async function createDatabase(locale: Locale, factory: DataStoreFactory, staticMessages?: MessagesDataset, staticEmojis?: Emoji[]): Promise{ const database = await initDatabaseInternal(locale, factory, staticMessages, staticEmojis); database.close(); diff --git a/packages/picmo/src/options.ts b/packages/picmo/src/options.ts index e5dd5dd1..40bf9b5a 100644 --- a/packages/picmo/src/options.ts +++ b/packages/picmo/src/options.ts @@ -4,6 +4,7 @@ import { NativeRenderer } from './renderers/native'; import { lightTheme } from './themes'; import en from './i18n/lang-en'; import { IndexedDbStoreFactory } from './data/IndexedDbStore'; +import { LocalStorageProvider } from './recents/LocalStorageProvider'; const defaultOptions: Partial = { renderer: new NativeRenderer(), @@ -12,20 +13,22 @@ const defaultOptions: Partial = { animate: true, - showSearch: true, showCategoryTabs: true, - showVariants: true, - showRecents: true, showPreview: true, + showRecents: true, + showSearch: true, + showVariants: true, emojisPerRow: 8, visibleRows: 6, emojiVersion: 'auto', - maxRecents: 50, i18n: en, locale: 'en', + maxRecents: 50, + recentsProvider: new LocalStorageProvider(), + custom: [] }; diff --git a/packages/picmo/src/recents.ts b/packages/picmo/src/recents.ts index 0e5ecc13..428eb9cb 100644 --- a/packages/picmo/src/recents.ts +++ b/packages/picmo/src/recents.ts @@ -1,30 +1,20 @@ -import { EmojiRecord } from './types'; +import { RecentsProvider } from './recents/RecentsProvider'; -const LOCAL_STORAGE_KEY = 'PicMo:recents'; +// Deprecated legacy interface to clear recents for backwards compatibility. +// This should not be used by new code; instead, call clear() on the +// RecentsProvider itself. -export function clear(): void { - localStorage.removeItem(LOCAL_STORAGE_KEY); -} +// It's a little odd and won't work well with a custom provider, but leaving it in +// so as not to create breaking changes. + +// @deprecated Remove in 6.0.0. -export function getRecents(maxCount: number): Array { - try { - const recents = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '[]'); - return recents.slice(0, maxCount); - } catch (error) { // localStorage is not available, no recents - return []; - } +let provider: RecentsProvider; + +export function setProvider(_provider: RecentsProvider) { + provider = _provider; } -export function addOrUpdateRecent(emoji: EmojiRecord, maxCount: number) { - // Add the new recent to the beginning of the list, removing it if it exists already - const recents = [ - emoji, - ...getRecents(maxCount).filter(recent => recent.hexcode !== emoji.hexcode) - ].slice(0, maxCount); - - try { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(recents)); - } catch (error) { - // localStorage is not available, no recents - } +export function clear(): void { + provider.clear(); } diff --git a/packages/picmo/src/recents/LocalStorageProvider.ts b/packages/picmo/src/recents/LocalStorageProvider.ts new file mode 100644 index 00000000..425137fa --- /dev/null +++ b/packages/picmo/src/recents/LocalStorageProvider.ts @@ -0,0 +1,7 @@ +import { WebStorageProvider } from './WebStorageProvider'; + +export class LocalStorageProvider extends WebStorageProvider { + constructor() { + super(localStorage); + } +} diff --git a/packages/picmo/src/recents/RecentsProvider.ts b/packages/picmo/src/recents/RecentsProvider.ts new file mode 100644 index 00000000..c5ebfb19 --- /dev/null +++ b/packages/picmo/src/recents/RecentsProvider.ts @@ -0,0 +1,7 @@ +import { EmojiRecord } from '../types'; + +export abstract class RecentsProvider { + abstract clear(): void; + abstract getRecents(maxCount: number): Array ; + abstract addOrUpdateRecent(emoji: EmojiRecord, maxCount: number): void; +} diff --git a/packages/picmo/src/recents/SessionStorageProvider.ts b/packages/picmo/src/recents/SessionStorageProvider.ts new file mode 100644 index 00000000..21f2535e --- /dev/null +++ b/packages/picmo/src/recents/SessionStorageProvider.ts @@ -0,0 +1,7 @@ +import { WebStorageProvider } from './WebStorageProvider'; + +export class SessionStorageProvider extends WebStorageProvider { + constructor() { + super(sessionStorage); + } +} diff --git a/packages/picmo/src/recents/WebStorageProvider.ts b/packages/picmo/src/recents/WebStorageProvider.ts new file mode 100644 index 00000000..3914335d --- /dev/null +++ b/packages/picmo/src/recents/WebStorageProvider.ts @@ -0,0 +1,40 @@ +import { EmojiRecord } from '../types'; +import { RecentsProvider } from './RecentsProvider'; + +const STORAGE_KEY = 'PicMo:recents'; + +export abstract class WebStorageProvider extends RecentsProvider { + storage: Storage; + + constructor(storage: Storage) { + super(); + this.storage = storage; + } + + clear(): void { + this.storage.removeItem(STORAGE_KEY); + } + + getRecents(maxCount: number): Array { + try { + const recents = JSON.parse(this.storage.getItem(STORAGE_KEY) ?? '[]'); + return recents.slice(0, maxCount); + } catch (error) { // storage is not available, no recents + return []; + } + } + + addOrUpdateRecent(emoji: EmojiRecord, maxCount: number) { + // Add the new recent to the beginning of the list, removing it if it exists already + const recents = [ + emoji, + ...this.getRecents(maxCount).filter(recent => recent.hexcode !== emoji.hexcode) + ].slice(0, maxCount); + + try { + this.storage.setItem(STORAGE_KEY, JSON.stringify(recents)); + } catch (error) { + console.warn('storage is not available, recent emojis will not be saved'); + } + } +} diff --git a/packages/picmo/src/recents/index.ts b/packages/picmo/src/recents/index.ts new file mode 100644 index 00000000..e3e97c7f --- /dev/null +++ b/packages/picmo/src/recents/index.ts @@ -0,0 +1,3 @@ +export { LocalStorageProvider } from './LocalStorageProvider'; +export { SessionStorageProvider } from './SessionStorageProvider'; +export { RecentsProvider } from './RecentsProvider'; diff --git a/packages/picmo/src/renderers/renderer.ts b/packages/picmo/src/renderers/renderer.ts index eb79ebd1..421b4156 100644 --- a/packages/picmo/src/renderers/renderer.ts +++ b/packages/picmo/src/renderers/renderer.ts @@ -38,9 +38,9 @@ export abstract class Renderer { const { content, resolver } = this.render(emoji, classNames); const contentElement = content instanceof HTMLElement ? content : content.el; - if (lazyLoader && resolver) { - return lazyLoader.lazyLoad(contentElement, resolver) - } + // if (lazyLoader && resolver) { + // return lazyLoader.lazyLoad(contentElement, resolver) + // } if (resolver) { resolver(); diff --git a/packages/picmo/src/types.ts b/packages/picmo/src/types.ts index b0ed1808..9ffa0474 100644 --- a/packages/picmo/src/types.ts +++ b/packages/picmo/src/types.ts @@ -2,6 +2,7 @@ import { Locale, MessagesDataset, Emoji } from 'emojibase'; import { Renderer } from './renderers/renderer'; import { Dictionary } from './i18n/bundle'; import { DataStoreFactory } from './data/DataStore'; +import { RecentsProvider } from './recents/RecentsProvider'; export type EmojiFocusTargetOffset = { row: 'first' | 'last' | number; @@ -11,15 +12,15 @@ export type EmojiFocusTargetOffset = { export type EmojiFocusTarget = EmojiFocusTargetOffset | string; export type EmojiRecord = { + custom?: boolean; + data?: any; emoji: string; + hexcode?: string; label: string; + order?: number; + skins?: EmojiRecord[]; tags?: string[]; url?: string; - skins?: EmojiRecord[]; - order?: number; - custom?: boolean; - hexcode?: string; - data?: any; version?: number; } @@ -55,140 +56,30 @@ export type Category = { } export type PickerOptions = { - /** - * The renderer to use for rendering and emitting emojis. - * This should be an instance of a subclass of `Renderer`. - * @default new NativeRenderer() - */ - renderer: Renderer; - - /** - * The color theme to use for the picker. Should be one of the class names - * exported by `picmo/themes`. - * @default `lightTheme` - */ - theme: string; - - className?: string; - - dataStore: DataStoreFactory; - - /** - * The DOM element that the picker will be appended to. Any existing children - * will be removed. - */ - rootElement: HTMLElement; - - /** - * Whether or not to show animated transitions in the picker. - * @default true - */ animate?: boolean; - - /** - * Whether or not to show recently used emojis. - * @default `true` - */ - showRecents: boolean; - - /** - * Whether or not to show the category tabs. - * @default true - */ - showCategoryTabs: boolean; - - /** - * Whether or not to show the search box. - * @default true - */ - showSearch: boolean; - - /** - * Whether or not to show the variants of emojis, where supported. - * If `false`, the default variant will always be emitted. - * @default true - */ - showVariants: boolean; - - /** - * Whether or not to show the preview of an emoji on hover or focus. - * @default true - */ - showPreview: boolean; - - /** - * Whether or not to autofocus the search field when the picker is rendered. - * @default false - */ autoFocusSearch: boolean; - - /** - * The maximum number of recent emojis that should be remembered. - * @default 50 - */ - maxRecents: number; - - /** - * The number of emojis that should be displayed per row. - * @default 8 - */ - emojisPerRow: number; - - /** - * The size of the emojis in the picker grid. - * Should be a valid CSS size string, e.g. `'1em'`. - * @default '2rem' - */ - emojiSize: string; - - /** - * The number of rows that should be visible in the picker's scroll area. - * @default 6 - */ - visibleRows: number; - - /** - * The categories to show in the picker. Leave undefined to show all categories. - * The categories will appear in the order that they are set in this array. - * @default undefined - */ categories?: CategoryKey[]; - - initialCategory?: CategoryKey; - - initialEmoji?: string; - - /** - * An array of custom emoji records to show in the picker. - */ + className?: string; custom?: CustomEmoji[]; - - /** - * The version of the Emoji standard to use, or 'auto' to - * automatically detect the supported version. - * @default 'auto' - */ + dataStore: DataStoreFactory; + emojiData?: Emoji[]; + emojiSize: string; + emojisPerRow: number; emojiVersion: number | 'auto'; - - /** - * A dictionary of i18n strings for UI messages. - * @default the built-in English strings - */ i18n: Dictionary; - - /** - * The locale to use for emoji labels. Allows all locales supported - * by Emojibase. - */ + initialCategory?: CategoryKey; + initialEmoji?: string; locale: Locale; - - /** - * The static emoji data to use instead of loading it from the CDN. - */ - emojiData?: Emoji[]; - - /** - * The static category data to use instead of loading it from the CDN. - */ + maxRecents: number; messages?: MessagesDataset; + recentsProvider: RecentsProvider; + renderer: Renderer; + rootElement: HTMLElement; + showCategoryTabs: boolean; + showPreview: boolean; + showRecents: boolean; + showSearch: boolean; + showVariants: boolean; + theme: string; + visibleRows: number; }; diff --git a/packages/picmo/src/views/EmojiArea.ts b/packages/picmo/src/views/EmojiArea.ts index 61437bc3..96dddbf4 100644 --- a/packages/picmo/src/views/EmojiArea.ts +++ b/packages/picmo/src/views/EmojiArea.ts @@ -158,7 +158,8 @@ export class EmojiArea extends View { category, showVariants: true, lazyLoader: this.lazyLoader, - emojiVersion: this.emojiVersion + emojiVersion: this.emojiVersion, + provider: this.options.recentsProvider }); } diff --git a/packages/picmo/src/views/EmojiPicker.ts b/packages/picmo/src/views/EmojiPicker.ts index 643d3b13..ae3cbbe9 100644 --- a/packages/picmo/src/views/EmojiPicker.ts +++ b/packages/picmo/src/views/EmojiPicker.ts @@ -7,7 +7,7 @@ import { EmojiPreview } from './Preview'; import { Search } from './Search'; import { VariantPopup } from './VariantPopup'; import { CategoryTabs } from './CategoryTabs'; -import { addOrUpdateRecent } from '../recents'; +import { setProvider } from '../recents'; import { DataStore } from '../data/DataStore'; import { EventCallback } from '../events'; @@ -70,6 +70,7 @@ export class EmojiPicker extends View { }; super.initialize(); + setProvider(this.options.recentsProvider); } /** @@ -87,6 +88,13 @@ export class EmojiPicker extends View { this.externalEvents.removeAll(); } + /** + * Convenience method to clear the recents using the configured recents provider. + */ + clearRecents() { + this.options.recentsProvider.clear(); + } + /** * Listens for a picker event. * @@ -410,7 +418,7 @@ export class EmojiPicker extends View { */ private async emitEmoji(emoji: EmojiRecord): Promise { this.externalEvents.emit('emoji:select', await this.renderer.doEmit(emoji)); - addOrUpdateRecent(emoji, this.options.maxRecents); + this.options.recentsProvider.addOrUpdateRecent(emoji, this.options.maxRecents); this.events.emit('recent:add', emoji); } } diff --git a/packages/picmo/src/views/RecentEmojiCategory.ts b/packages/picmo/src/views/RecentEmojiCategory.ts index a6daff50..1d2fc5be 100644 --- a/packages/picmo/src/views/RecentEmojiCategory.ts +++ b/packages/picmo/src/views/RecentEmojiCategory.ts @@ -4,19 +4,22 @@ import { RecentEmojiContainer } from './RecentEmojiContainer'; import { Category, EmojiRecord } from '../types'; import { LazyLoader } from '../LazyLoader'; import { categoryIcons } from '../icons'; -import { getRecents, clear } from '../recents'; import template from './RecentEmojiCategory.template'; import classes from './EmojiCategory.scss'; +import { RecentsProvider } from '../recents/RecentsProvider'; type RecentEmojiCategoryOptions = { category: Category; + provider: RecentsProvider; lazyLoader?: LazyLoader; }; export class RecentEmojiCategory extends BaseEmojiCategory { emojiContainer: RecentEmojiContainer; + private provider: RecentsProvider; - constructor({ category, lazyLoader }: RecentEmojiCategoryOptions) { + constructor({ category, lazyLoader, provider }: RecentEmojiCategoryOptions) { super({ category, showVariants: false, lazyLoader, template }); + this.provider = provider; } initialize() { @@ -38,7 +41,7 @@ export class RecentEmojiCategory extends BaseEmojiCategory { } async render(): Promise { - const recents = getRecents(this.options.maxRecents); + const recents = this.provider?.getRecents(this.options.maxRecents); this.emojiContainer = this.viewFactory.create(RecentEmojiContainer, { emojis: recents,