mirror of
https://github.com/iconify/iconify.git
synced 2024-12-04 18:23:17 +00:00
feat: custom loader for one icon
This commit is contained in:
parent
1fad38b291
commit
84f87bacb3
@ -14,6 +14,10 @@ import type {
|
||||
IconifyAPICustomQueryParams,
|
||||
} from './modules';
|
||||
import type { IconifyIcon } from '@iconify/types';
|
||||
import type {
|
||||
IconifyCustomIconLoader,
|
||||
IconifyCustomIconsLoader,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Iconify API functions
|
||||
@ -41,6 +45,24 @@ export interface IconifyAPIFunctions {
|
||||
provider: string,
|
||||
customConfig: PartialIconifyAPIConfig
|
||||
) => boolean;
|
||||
|
||||
/**
|
||||
* Set custom loader for multple icons
|
||||
*/
|
||||
setCustomIconsLoader: (
|
||||
callback: IconifyCustomIconsLoader,
|
||||
prefix: string,
|
||||
provider?: string
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Set custom loader for one icon
|
||||
*/
|
||||
setCustomIconLoader: (
|
||||
callback: IconifyCustomIconLoader,
|
||||
prefix: string,
|
||||
provider?: string
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,7 +91,8 @@ function checkIconNamesForAPI(icons: string[]): CheckIconNames {
|
||||
function parseLoaderResponse(
|
||||
storage: IconStorageWithAPI,
|
||||
icons: string[],
|
||||
data: unknown
|
||||
data: unknown,
|
||||
isAPIResponse: boolean
|
||||
) {
|
||||
function checkMissing() {
|
||||
const pending = storage.pendingIcons;
|
||||
@ -119,7 +120,9 @@ function parseLoaderResponse(
|
||||
}
|
||||
|
||||
// Cache API response
|
||||
storeInBrowserStorage(storage, data as IconifyJSON);
|
||||
if (isAPIResponse) {
|
||||
storeInBrowserStorage(storage, data as IconifyJSON);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
@ -132,6 +135,28 @@ function parseLoaderResponse(
|
||||
loadedNewIcons(storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle response that can be async
|
||||
*/
|
||||
function parsePossiblyAsyncResponse<T>(
|
||||
response: T | null | Promise<T | null>,
|
||||
callback: (data: T | null) => void
|
||||
): void {
|
||||
if (response instanceof Promise) {
|
||||
// Custom loader is async
|
||||
response
|
||||
.then((data) => {
|
||||
callback(data);
|
||||
})
|
||||
.catch(() => {
|
||||
callback(null);
|
||||
});
|
||||
} else {
|
||||
// Sync loader
|
||||
callback(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load icons
|
||||
*/
|
||||
@ -158,22 +183,34 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for custom loader
|
||||
if (storage.customLoader) {
|
||||
const response = storage.customLoader(icons, prefix, provider);
|
||||
if (response instanceof Promise) {
|
||||
// Custom loader is async
|
||||
response
|
||||
.then((data) => {
|
||||
parseLoaderResponse(storage, icons, data);
|
||||
})
|
||||
.catch(() => {
|
||||
parseLoaderResponse(storage, icons, null);
|
||||
});
|
||||
} else {
|
||||
// Sync loader
|
||||
parseLoaderResponse(storage, icons, response);
|
||||
}
|
||||
// Check for custom loader for multiple icons
|
||||
const customIconLoader = storage.loadIcon;
|
||||
if (storage.loadIcons && (icons.length > 1 || !customIconLoader)) {
|
||||
parsePossiblyAsyncResponse(
|
||||
storage.loadIcons(icons, prefix, provider),
|
||||
(data) => {
|
||||
parseLoaderResponse(storage, icons, data, false);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for custom loader for one icon
|
||||
if (customIconLoader) {
|
||||
icons.forEach((name) => {
|
||||
const response = customIconLoader(name, prefix, provider);
|
||||
parsePossiblyAsyncResponse(response, (data) => {
|
||||
const iconSet: IconifyJSON | null = data
|
||||
? {
|
||||
prefix,
|
||||
icons: {
|
||||
[name]: data,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
parseLoaderResponse(storage, [name], iconSet, false);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -183,7 +220,7 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
|
||||
|
||||
if (invalid.length) {
|
||||
// Invalid icons
|
||||
parseLoaderResponse(storage, invalid, null);
|
||||
parseLoaderResponse(storage, invalid, null, false);
|
||||
}
|
||||
if (!valid.length) {
|
||||
// No valid icons to load
|
||||
@ -196,7 +233,7 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
|
||||
: null;
|
||||
if (!api) {
|
||||
// API module not found
|
||||
parseLoaderResponse(storage, valid, null);
|
||||
parseLoaderResponse(storage, valid, null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -204,7 +241,7 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
|
||||
const params = api.prepare(provider, prefix, valid);
|
||||
params.forEach((item) => {
|
||||
sendAPIQuery(provider, item, (data) => {
|
||||
parseLoaderResponse(storage, item.icons, data);
|
||||
parseLoaderResponse(storage, item.icons, data, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,15 +1,35 @@
|
||||
import { getStorage } from '../storage/storage.js';
|
||||
import type { IconifyCustomLoader, IconStorageWithAPI } from './types.js';
|
||||
import { getStorage } from '../storage/storage';
|
||||
import type {
|
||||
IconifyCustomIconLoader,
|
||||
IconifyCustomIconsLoader,
|
||||
IconStorageWithAPI,
|
||||
} from './types';
|
||||
|
||||
// Custom loaders
|
||||
// You can set only one of these loaders, whichever is more suitable for your use case.
|
||||
|
||||
/**
|
||||
* Set custom loader
|
||||
* Set custom loader for multiple icons
|
||||
*/
|
||||
export function setCustomLoader(
|
||||
loader: IconifyCustomLoader,
|
||||
export function setCustomIconsLoader(
|
||||
loader: IconifyCustomIconsLoader,
|
||||
prefix: string,
|
||||
provider?: string
|
||||
): void {
|
||||
// Assign loader directly to storage
|
||||
(getStorage(provider || '', prefix) as IconStorageWithAPI).customLoader =
|
||||
(getStorage(provider || '', prefix) as IconStorageWithAPI).loadIcons =
|
||||
loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom loader for one icon
|
||||
*/
|
||||
export function setCustomIconLoader(
|
||||
loader: IconifyCustomIconLoader,
|
||||
prefix: string,
|
||||
provider?: string
|
||||
): void {
|
||||
// Assign loader directly to storage
|
||||
(getStorage(provider || '', prefix) as IconStorageWithAPI).loadIcon =
|
||||
loader;
|
||||
}
|
||||
|
@ -4,17 +4,26 @@ import type {
|
||||
} from './icons';
|
||||
import type { SortedIcons } from '../icon/sort';
|
||||
import type { IconStorage } from '../storage/storage';
|
||||
import { IconifyJSON } from '@iconify/types';
|
||||
import { IconifyIcon, IconifyJSON } from '@iconify/types';
|
||||
|
||||
/**
|
||||
* Custom icons loader
|
||||
*/
|
||||
export type IconifyCustomLoader = (
|
||||
export type IconifyCustomIconsLoader = (
|
||||
icons: string[],
|
||||
prefix: string,
|
||||
provider: string
|
||||
) => Promise<IconifyJSON | null> | IconifyJSON | null;
|
||||
|
||||
/**
|
||||
* Custom loader for one icon
|
||||
*/
|
||||
export type IconifyCustomIconLoader = (
|
||||
name: string,
|
||||
prefix: string,
|
||||
provider: string
|
||||
) => Promise<IconifyIcon | null> | IconifyIcon | null;
|
||||
|
||||
/**
|
||||
* Storage for callbacks
|
||||
*/
|
||||
@ -36,8 +45,18 @@ export interface APICallbackItem {
|
||||
* Add custom stuff to storage
|
||||
*/
|
||||
export interface IconStorageWithAPI extends IconStorage {
|
||||
// Custom loader
|
||||
customLoader?: IconifyCustomLoader;
|
||||
/**
|
||||
* Custom loaders
|
||||
*
|
||||
* If custom loader is set, API module will not be used to load icons.
|
||||
*
|
||||
* You can set only one of these loaders.
|
||||
*
|
||||
* If both loaders are set, loader for one icon will be used when requesting only once icon,
|
||||
* loader for multiple icons will be used when requesting multiple icons.
|
||||
*/
|
||||
loadIcons?: IconifyCustomIconsLoader;
|
||||
loadIcon?: IconifyCustomIconLoader;
|
||||
|
||||
/**
|
||||
* List of icons that are being loaded, added to storage
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { defaultIconProps } from '@iconify/utils';
|
||||
import { loadIcons, loadIcon } from '../../lib/api/icons';
|
||||
import { setCustomLoader } from '../../lib/api/loaders';
|
||||
import {
|
||||
setCustomIconsLoader,
|
||||
setCustomIconLoader,
|
||||
} from '../../lib/api/loaders';
|
||||
import { listIcons } from '../../lib/storage/storage';
|
||||
|
||||
describe('Testing API loadIcons', () => {
|
||||
@ -10,13 +13,13 @@ describe('Testing API loadIcons', () => {
|
||||
return `loader-test-${prefixCounter < 10 ? '0' : ''}${prefixCounter}`;
|
||||
}
|
||||
|
||||
it('Custom async loader with loadIcon', () => {
|
||||
it('Custom async loader for multiple icons with loadIcon', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const provider = nextPrefix();
|
||||
const prefix = nextPrefix();
|
||||
|
||||
// Set loader
|
||||
setCustomLoader(
|
||||
setCustomIconsLoader(
|
||||
(icons, requestedPrefix, requestedProvider) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
@ -72,13 +75,13 @@ describe('Testing API loadIcons', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Custom sync loader with loadIcon', () => {
|
||||
it('Custom sync loader for multiple icons with loadIcon', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const provider = nextPrefix();
|
||||
const prefix = nextPrefix();
|
||||
|
||||
// Set loader
|
||||
setCustomLoader(
|
||||
setCustomIconsLoader(
|
||||
(icons, requestedPrefix, requestedProvider) => {
|
||||
try {
|
||||
// Check params
|
||||
@ -129,14 +132,14 @@ describe('Testing API loadIcons', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Missing icons', () => {
|
||||
it('Loader multiple icons with missing icons', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const provider = nextPrefix();
|
||||
const prefix1 = nextPrefix();
|
||||
const prefix2 = nextPrefix();
|
||||
|
||||
// Set loaders: one is sync, one is async
|
||||
setCustomLoader(
|
||||
setCustomIconsLoader(
|
||||
(icons, requestedPrefix, requestedProvider) => {
|
||||
try {
|
||||
// Check params
|
||||
@ -161,7 +164,7 @@ describe('Testing API loadIcons', () => {
|
||||
prefix1,
|
||||
provider
|
||||
);
|
||||
setCustomLoader(
|
||||
setCustomIconsLoader(
|
||||
(icons, requestedPrefix, requestedProvider) => {
|
||||
try {
|
||||
// Check params
|
||||
@ -197,7 +200,7 @@ describe('Testing API loadIcons', () => {
|
||||
provider
|
||||
);
|
||||
|
||||
// Load icon
|
||||
// Load icons
|
||||
loadIcons(
|
||||
[
|
||||
`${provider}:${prefix1}:icon1`,
|
||||
@ -250,4 +253,329 @@ describe('Testing API loadIcons', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Custom async loader for one icon with loadIcon', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const provider = nextPrefix();
|
||||
const prefix = nextPrefix();
|
||||
|
||||
// Set loader
|
||||
setCustomIconLoader(
|
||||
(name, requestedPrefix, requestedProvider) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Check params
|
||||
expect(name).toBe('icon1');
|
||||
expect(requestedPrefix).toBe(prefix);
|
||||
expect(requestedProvider).toBe(provider);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send data
|
||||
resolve({
|
||||
body: '<path d="" />',
|
||||
});
|
||||
});
|
||||
},
|
||||
prefix,
|
||||
provider
|
||||
);
|
||||
|
||||
// Load icon
|
||||
loadIcon(provider + ':' + prefix + ':icon1')
|
||||
.then((data) => {
|
||||
try {
|
||||
// Test response
|
||||
expect(data).toEqual({
|
||||
...defaultIconProps,
|
||||
body: '<path d="" />',
|
||||
});
|
||||
|
||||
// Check storage
|
||||
expect(listIcons(provider, prefix)).toEqual([
|
||||
`@${provider}:${prefix}:icon1`,
|
||||
]);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
|
||||
it('Loader multiple icons with loader for one icon', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const provider = nextPrefix();
|
||||
const prefix1 = nextPrefix();
|
||||
const prefix2 = nextPrefix();
|
||||
|
||||
const iconsToTest: Record<string, Set<string>> = {
|
||||
[prefix1]: new Set(['icon1']),
|
||||
[prefix2]: new Set(['Icon_2', 'BadIcon']),
|
||||
};
|
||||
|
||||
// Set loaders: one is sync, one is async
|
||||
setCustomIconLoader(
|
||||
(requestedName, requestedPrefix, requestedProvider) => {
|
||||
try {
|
||||
// Check params
|
||||
expect(requestedPrefix).toBe(prefix1);
|
||||
expect(requestedProvider).toBe(provider);
|
||||
expect(
|
||||
iconsToTest[prefix1].has(requestedName)
|
||||
).toBeTruthy();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send data
|
||||
return {
|
||||
body: `<g data-name="${requestedName}" />`,
|
||||
};
|
||||
},
|
||||
prefix1,
|
||||
provider
|
||||
);
|
||||
setCustomIconLoader(
|
||||
(requestedName, requestedPrefix, requestedProvider) => {
|
||||
try {
|
||||
// Check params
|
||||
expect(requestedPrefix).toBe(prefix2);
|
||||
expect(requestedProvider).toBe(provider);
|
||||
expect(
|
||||
iconsToTest[prefix2].has(requestedName)
|
||||
).toBeTruthy();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send data asynchronously, without 'BadIcon'
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(
|
||||
requestedName === 'BadIcon'
|
||||
? null
|
||||
: {
|
||||
body: `<g data-name="${requestedName}" />`,
|
||||
}
|
||||
);
|
||||
}, 150);
|
||||
});
|
||||
},
|
||||
prefix2,
|
||||
provider
|
||||
);
|
||||
|
||||
// Load icons
|
||||
loadIcons(
|
||||
[
|
||||
`${provider}:${prefix1}:icon1`,
|
||||
`${provider}:${prefix2}:Icon_2`,
|
||||
`${provider}:${prefix2}:BadIcon`,
|
||||
],
|
||||
(loaded, missing, pending) => {
|
||||
if (pending.length) {
|
||||
// Could be called before all icons are loaded because multiple async requests
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Test response
|
||||
expect(loaded).toEqual([
|
||||
{
|
||||
provider,
|
||||
prefix: prefix1,
|
||||
name: 'icon1',
|
||||
},
|
||||
{
|
||||
provider,
|
||||
prefix: prefix2,
|
||||
name: 'Icon_2',
|
||||
},
|
||||
]);
|
||||
expect(missing).toEqual([
|
||||
{
|
||||
provider,
|
||||
prefix: prefix2,
|
||||
name: 'BadIcon',
|
||||
},
|
||||
]);
|
||||
|
||||
// Check storage
|
||||
expect(listIcons(provider, prefix1)).toEqual([
|
||||
`@${provider}:${prefix1}:icon1`,
|
||||
]);
|
||||
expect(listIcons(provider, prefix2)).toEqual([
|
||||
`@${provider}:${prefix2}:Icon_2`,
|
||||
]);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Loaders for one and multiple icons, requesting one icon', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const provider = nextPrefix();
|
||||
const prefix = nextPrefix();
|
||||
const name = 'TestIcon';
|
||||
|
||||
// Set loaders: one is sync, one is async
|
||||
setCustomIconLoader(
|
||||
(requestedName, requestedPrefix, requestedProvider) => {
|
||||
try {
|
||||
// Check params
|
||||
expect(requestedPrefix).toBe(prefix);
|
||||
expect(requestedProvider).toBe(provider);
|
||||
expect(requestedName).toBe(name);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send data
|
||||
return {
|
||||
body: `<g data-name="${requestedName}" />`,
|
||||
};
|
||||
},
|
||||
prefix,
|
||||
provider
|
||||
);
|
||||
setCustomIconsLoader(
|
||||
() => {
|
||||
reject(
|
||||
new Error(
|
||||
'Loader for multple icons should not be called'
|
||||
)
|
||||
);
|
||||
return null;
|
||||
},
|
||||
prefix,
|
||||
provider
|
||||
);
|
||||
|
||||
// Load icon
|
||||
loadIcons(
|
||||
[`${provider}:${prefix}:${name}`],
|
||||
(loaded, missing, pending) => {
|
||||
try {
|
||||
// Test response
|
||||
expect(loaded).toEqual([
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name,
|
||||
},
|
||||
]);
|
||||
expect(missing).toEqual([]);
|
||||
expect(pending).toEqual([]);
|
||||
|
||||
// Check storage
|
||||
expect(listIcons(provider, prefix)).toEqual([
|
||||
`@${provider}:${prefix}:${name}`,
|
||||
]);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Loaders for one and multiple icons, requesting multiple icons', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const provider = nextPrefix();
|
||||
const prefix = nextPrefix();
|
||||
|
||||
// Set loaders: one is sync, one is async
|
||||
setCustomIconsLoader(
|
||||
(requestedNames, requestedPrefix, requestedProvider) => {
|
||||
try {
|
||||
// Check params
|
||||
expect(requestedPrefix).toBe(prefix);
|
||||
expect(requestedProvider).toBe(provider);
|
||||
expect(requestedNames).toEqual(['Icon_1', 'Icon_2']);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send data
|
||||
return {
|
||||
prefix,
|
||||
icons: {
|
||||
Icon_1: {
|
||||
body: `<g data-name="Icon_1" />`,
|
||||
},
|
||||
Icon_3: {
|
||||
body: `<g data-name="Icon_3" />`,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
prefix,
|
||||
provider
|
||||
);
|
||||
setCustomIconLoader(
|
||||
() => {
|
||||
reject(
|
||||
new Error('Loader for one icon should not be called')
|
||||
);
|
||||
return null;
|
||||
},
|
||||
prefix,
|
||||
provider
|
||||
);
|
||||
|
||||
// Load icon
|
||||
loadIcons(
|
||||
[
|
||||
`${provider}:${prefix}:Icon_1`,
|
||||
`${provider}:${prefix}:Icon_2`,
|
||||
],
|
||||
(loaded, missing, pending) => {
|
||||
try {
|
||||
// Test response
|
||||
expect(loaded).toEqual([
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'Icon_1',
|
||||
},
|
||||
]);
|
||||
expect(missing).toEqual([
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'Icon_2',
|
||||
},
|
||||
]);
|
||||
expect(pending).toEqual([]);
|
||||
|
||||
// Check storage
|
||||
expect(listIcons(provider, prefix)).toEqual([
|
||||
`@${provider}:${prefix}:Icon_1`,
|
||||
`@${provider}:${prefix}:Icon_3`,
|
||||
]);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user