react-inlinesvg 4.1.8 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -34
- package/dist/cache-NLB60kAd.d.mts +142 -0
- package/dist/cache-NLB60kAd.d.ts +142 -0
- package/dist/index.d.mts +7 -73
- package/dist/index.d.ts +7 -73
- package/dist/index.js +284 -202
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +280 -206
- package/dist/index.mjs.map +1 -1
- package/dist/provider.d.mts +5 -3
- package/dist/provider.d.ts +5 -3
- package/dist/provider.js +187 -6
- package/dist/provider.js.map +1 -1
- package/dist/provider.mjs +176 -6
- package/dist/provider.mjs.map +1 -1
- package/package.json +25 -41
- package/src/index.tsx +29 -261
- package/src/modules/cache.ts +100 -70
- package/src/modules/helpers.ts +1 -9
- package/src/modules/hooks.tsx +6 -1
- package/src/modules/useInlineSVG.ts +272 -0
- package/src/modules/utils.ts +36 -1
- package/src/provider.tsx +10 -7
- package/src/types.ts +69 -3
- package/src/global.d.ts +0 -6
package/dist/provider.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/modules/helpers.ts","../src/provider.tsx"],"sourcesContent":["import type { PlainObject } from '../types';\n\nfunction randomCharacter(character: string) {\n return character[Math.floor(Math.random() * character.length)];\n}\n\nexport function canUseDOM(): boolean {\n return !!(typeof window !== 'undefined' && window.document?.createElement);\n}\n\nexport function isSupportedEnvironment(): boolean {\n return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;\n}\n\n/**\n * Remove properties from an object\n */\nexport function omit<T extends PlainObject, K extends keyof T>(\n input: T,\n ...filter: K[]\n): Omit<T, K> {\n const output: any = {};\n\n for (const key in input) {\n if ({}.hasOwnProperty.call(input, key)) {\n if (!filter.includes(key as unknown as K)) {\n output[key] = input[key];\n }\n }\n }\n\n return output as Omit<T, K>;\n}\n\nexport function randomString(length: number): string {\n const letters = 'abcdefghijklmnopqrstuvwxyz';\n const numbers = '1234567890';\n const charset = `${letters}${letters.toUpperCase()}${numbers}`;\n\n let R = '';\n\n for (let index = 0; index < length; index++) {\n R += randomCharacter(charset);\n }\n\n return R;\n}\n\nexport async function request(url: string, options?: RequestInit) {\n const response = await fetch(url, options);\n const contentType = response.headers.get('content-type');\n const [fileType] = (contentType ?? '').split(/ ?; ?/);\n\n if (response.status > 299) {\n throw new Error('Not found');\n }\n\n if (!['image/svg+xml', 'text/plain'].some(d => fileType.includes(d))) {\n throw new Error(`Content type isn't valid: ${fileType}`);\n }\n\n return response.text();\n}\n\nexport function sleep(seconds = 1) {\n return new Promise(resolve => {\n setTimeout(resolve, seconds * 1000);\n });\n}\n\nexport function supportsInlineSVG(): boolean {\n /* c8 ignore next 3 */\n if (!document) {\n return false;\n }\n\n const div = document.createElement('div');\n\n div.innerHTML = '<svg />';\n const svg = div.firstChild as SVGSVGElement;\n\n return !!svg && svg.namespaceURI === 'http://www.w3.org/2000/svg';\n}\n","import { ReactNode } from 'react';\n\nimport { canUseDOM } from './modules/helpers';\n\ninterface Props {\n children: ReactNode;\n name?: string;\n}\n\nexport default function CacheProvider({ children, name }: Props) {\n if (canUseDOM()) {\n window.REACT_INLINESVG_CACHE_NAME = name;\n window.REACT_INLINESVG_PERSISTENT_CACHE = true;\n }\n\n return children;\n}\n"],"mappings":";;;AAMO,SAAS,YAAqB;AACnC,SAAO,CAAC,EAAE,OAAO,WAAW,eAAe,OAAO,UAAU;AAC9D;;;ACCe,SAAR,cAA+B,EAAE,UAAU,KAAK,GAAU;AAC/D,MAAI,UAAU,GAAG;AACf,WAAO,6BAA6B;AACpC,WAAO,mCAAmC;AAAA,EAC5C;AAEA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx","../src/config.ts","../src/modules/helpers.ts","../src/modules/cache.ts"],"sourcesContent":["import React, { createContext, ReactNode, useContext, useState } from 'react';\n\nimport CacheStore from './modules/cache';\n\nconst CacheContext = createContext<CacheStore | null>(null);\n\ninterface Props {\n children: ReactNode;\n name?: string;\n}\n\nexport default function CacheProvider({ children, name }: Props) {\n const [store] = useState(() => new CacheStore({ name, persistent: true }));\n\n return <CacheContext.Provider value={store}>{children}</CacheContext.Provider>;\n}\n\nexport function useCacheStore(): CacheStore | null {\n return useContext(CacheContext);\n}\n","export const CACHE_NAME = 'react-inlinesvg';\nexport const CACHE_MAX_RETRIES = 10;\n\nexport const STATUS = {\n IDLE: 'idle',\n LOADING: 'loading',\n LOADED: 'loaded',\n FAILED: 'failed',\n READY: 'ready',\n UNSUPPORTED: 'unsupported',\n} as const;\n","function randomCharacter(character: string) {\n return character[Math.floor(Math.random() * character.length)];\n}\n\nexport function canUseDOM(): boolean {\n return !!(typeof window !== 'undefined' && window.document?.createElement);\n}\n\nexport function isSupportedEnvironment(): boolean {\n return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;\n}\n\n/**\n * Remove properties from an object\n */\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(\n input: T,\n ...filter: K[]\n): Omit<T, K> {\n const output: any = {};\n\n for (const key in input) {\n if ({}.hasOwnProperty.call(input, key)) {\n if (!filter.includes(key as unknown as K)) {\n output[key] = input[key];\n }\n }\n }\n\n return output as Omit<T, K>;\n}\n\nexport function randomString(length: number): string {\n const letters = 'abcdefghijklmnopqrstuvwxyz';\n const numbers = '1234567890';\n const charset = `${letters}${letters.toUpperCase()}${numbers}`;\n\n let R = '';\n\n for (let index = 0; index < length; index++) {\n R += randomCharacter(charset);\n }\n\n return R;\n}\n\nexport async function request(url: string, options?: RequestInit) {\n const response = await fetch(url, options);\n const contentType = response.headers.get('content-type');\n const [fileType] = (contentType ?? '').split(/ ?; ?/);\n\n if (response.status > 299) {\n throw new Error('Not found');\n }\n\n if (!['image/svg+xml', 'text/plain'].some(d => fileType.includes(d))) {\n throw new Error(`Content type isn't valid: ${fileType}`);\n }\n\n return response.text();\n}\n\nexport function supportsInlineSVG(): boolean {\n /* c8 ignore next 3 */\n if (!document) {\n return false;\n }\n\n const div = document.createElement('div');\n\n div.innerHTML = '<svg />';\n const svg = div.firstChild as SVGSVGElement;\n\n return !!svg && svg.namespaceURI === 'http://www.w3.org/2000/svg';\n}\n","import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from '../config';\nimport { StorageItem } from '../types';\n\nimport { canUseDOM, request } from './helpers';\n\nexport interface CacheStoreOptions {\n name?: string;\n persistent?: boolean;\n}\n\nexport default class CacheStore {\n private cacheApi: Cache | undefined;\n private readonly cacheStore: Map<string, StorageItem>;\n private readonly subscribers: Array<() => void> = [];\n public isReady = false;\n\n constructor(options: CacheStoreOptions = {}) {\n const { name = CACHE_NAME, persistent = false } = options;\n\n this.cacheStore = new Map<string, StorageItem>();\n\n const usePersistentCache = persistent && canUseDOM() && 'caches' in window;\n\n if (usePersistentCache) {\n // eslint-disable-next-line promise/catch-or-return\n caches\n .open(name)\n .then(cache => {\n this.cacheApi = cache;\n })\n .catch(error => {\n // eslint-disable-next-line no-console\n console.error(`Failed to open cache: ${error.message}`);\n this.cacheApi = undefined;\n })\n .finally(() => {\n this.isReady = true;\n // Copy to avoid mutation issues\n const callbacks = [...this.subscribers];\n\n // Clear array efficiently\n this.subscribers.length = 0;\n\n callbacks.forEach(callback => {\n try {\n callback();\n } catch (error: any) {\n // eslint-disable-next-line no-console\n console.error(`Error in CacheStore subscriber callback: ${error.message}`);\n }\n });\n });\n } else {\n this.isReady = true;\n }\n }\n\n public onReady(callback: () => void): () => void {\n if (this.isReady) {\n callback();\n\n return () => {};\n }\n\n this.subscribers.push(callback);\n\n return () => {\n const index = this.subscribers.indexOf(callback);\n\n if (index >= 0) {\n this.subscribers.splice(index, 1);\n }\n };\n }\n\n private waitForReady(): Promise<void> {\n if (this.isReady) {\n return Promise.resolve();\n }\n\n return new Promise(resolve => {\n this.onReady(resolve);\n });\n }\n\n public async get(url: string, fetchOptions?: RequestInit) {\n await this.fetchAndCache(url, fetchOptions);\n\n return this.cacheStore.get(url)?.content ?? '';\n }\n\n public getContent(url: string): string {\n return this.cacheStore.get(url)?.content ?? '';\n }\n\n public set(url: string, data: StorageItem) {\n this.cacheStore.set(url, data);\n }\n\n public isCached(url: string) {\n return this.cacheStore.get(url)?.status === STATUS.LOADED;\n }\n\n private async fetchAndCache(url: string, fetchOptions?: RequestInit) {\n if (!this.isReady) {\n await this.waitForReady();\n }\n\n const cache = this.cacheStore.get(url);\n\n if (cache?.status === STATUS.LOADED) {\n return;\n }\n\n if (cache?.status === STATUS.LOADING) {\n await this.handleLoading(url, fetchOptions?.signal || undefined, async () => {\n this.cacheStore.set(url, { content: '', status: STATUS.IDLE });\n await this.fetchAndCache(url, fetchOptions);\n });\n\n return;\n }\n\n this.cacheStore.set(url, { content: '', status: STATUS.LOADING });\n\n try {\n const content = this.cacheApi\n ? await this.fetchFromPersistentCache(url, fetchOptions)\n : await request(url, fetchOptions);\n\n this.cacheStore.set(url, { content, status: STATUS.LOADED });\n } catch (error: any) {\n this.cacheStore.set(url, { content: '', status: STATUS.FAILED });\n throw error;\n }\n }\n\n private async fetchFromPersistentCache(url: string, fetchOptions?: RequestInit): Promise<string> {\n const data = await this.cacheApi?.match(url);\n\n if (data) {\n return data.text();\n }\n\n await this.cacheApi?.add(new Request(url, fetchOptions));\n\n const response = await this.cacheApi?.match(url);\n\n return (await response?.text()) ?? '';\n }\n\n private async handleLoading(\n url: string,\n signal: AbortSignal | undefined,\n callback: () => Promise<void>,\n ) {\n for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) {\n if (signal?.aborted) {\n throw signal.reason instanceof Error\n ? signal.reason\n : new DOMException('The operation was aborted.', 'AbortError');\n }\n\n if (this.cacheStore.get(url)?.status !== STATUS.LOADING) {\n return;\n }\n\n await sleep(0.1);\n }\n\n await callback();\n }\n\n public keys(): Array<string> {\n return [...this.cacheStore.keys()];\n }\n\n public data(): Array<Record<string, StorageItem>> {\n return [...this.cacheStore.entries()].map(([key, value]) => ({ [key]: value }));\n }\n\n public async delete(url: string) {\n if (this.cacheApi) {\n await this.cacheApi.delete(url);\n }\n\n this.cacheStore.delete(url);\n }\n\n public async clear() {\n if (this.cacheApi) {\n const keys = await this.cacheApi.keys();\n\n await Promise.allSettled(keys.map(key => this.cacheApi!.delete(key)));\n }\n\n this.cacheStore.clear();\n }\n}\n\nfunction sleep(seconds = 1) {\n return new Promise(resolve => {\n setTimeout(resolve, seconds * 1000);\n });\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-inlinesvg",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "An SVG loader for React",
|
|
5
5
|
"author": "Gil Barbara <gilbarbara@gmail.com>",
|
|
6
6
|
"contributors": [
|
|
@@ -54,35 +54,37 @@
|
|
|
54
54
|
"react-from-dom": "^0.7.5"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@arethetypeswrong/cli": "^0.
|
|
58
|
-
"@gilbarbara/eslint-config": "^
|
|
57
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
58
|
+
"@gilbarbara/eslint-config": "^1.1.0",
|
|
59
59
|
"@gilbarbara/prettier-config": "^1.0.0",
|
|
60
|
-
"@gilbarbara/tsconfig": "^0.
|
|
61
|
-
"@size-limit/preset-small-lib": "^
|
|
62
|
-
"@testing-library/jest-dom": "^6.
|
|
63
|
-
"@testing-library/react": "^16.
|
|
64
|
-
"@types/node": "^
|
|
65
|
-
"@types/react": "^
|
|
66
|
-
"@types/react-dom": "^
|
|
67
|
-
"@vitejs/plugin-react": "^
|
|
68
|
-
"@vitest/coverage-v8": "^
|
|
60
|
+
"@gilbarbara/tsconfig": "^1.0.0",
|
|
61
|
+
"@size-limit/preset-small-lib": "^12.0.1",
|
|
62
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
63
|
+
"@testing-library/react": "^16.3.2",
|
|
64
|
+
"@types/node": "^24.12.0",
|
|
65
|
+
"@types/react": "^19.2.14",
|
|
66
|
+
"@types/react-dom": "^19.2.3",
|
|
67
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
68
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
69
69
|
"browser-cache-mock": "^0.1.7",
|
|
70
|
-
"del-cli": "^
|
|
70
|
+
"del-cli": "^7.0.0",
|
|
71
|
+
"eslint": "^9.39.4",
|
|
71
72
|
"fix-tsup-cjs": "^1.2.0",
|
|
72
73
|
"http-server": "^14.1.1",
|
|
73
74
|
"husky": "^9.1.7",
|
|
74
|
-
"jest-extended": "^
|
|
75
|
-
"jsdom": "^
|
|
76
|
-
"
|
|
77
|
-
"react
|
|
75
|
+
"jest-extended": "^7.0.0",
|
|
76
|
+
"jsdom": "^29.0.1",
|
|
77
|
+
"prettier": "^3.8.1",
|
|
78
|
+
"react": "^19.2.4",
|
|
79
|
+
"react-dom": "^19.2.4",
|
|
78
80
|
"repo-tools": "^0.3.1",
|
|
79
|
-
"size-limit": "^
|
|
80
|
-
"start-server-and-test": "^
|
|
81
|
+
"size-limit": "^12.0.1",
|
|
82
|
+
"start-server-and-test": "^3.0.0",
|
|
81
83
|
"ts-node": "^10.9.2",
|
|
82
|
-
"tsup": "^8.
|
|
83
|
-
"typescript": "^5.
|
|
84
|
-
"vitest": "^
|
|
85
|
-
"vitest-fetch-mock": "^0.4.
|
|
84
|
+
"tsup": "^8.5.1",
|
|
85
|
+
"typescript": "^5.9.3",
|
|
86
|
+
"vitest": "^4.1.2",
|
|
87
|
+
"vitest-fetch-mock": "^0.4.5"
|
|
86
88
|
},
|
|
87
89
|
"scripts": {
|
|
88
90
|
"build": "pnpm run clean && tsup && fix-tsup-cjs",
|
|
@@ -117,24 +119,6 @@
|
|
|
117
119
|
"sourcemap": true,
|
|
118
120
|
"splitting": false
|
|
119
121
|
},
|
|
120
|
-
"eslintConfig": {
|
|
121
|
-
"extends": [
|
|
122
|
-
"@gilbarbara/eslint-config"
|
|
123
|
-
],
|
|
124
|
-
"overrides": [
|
|
125
|
-
{
|
|
126
|
-
"files": [
|
|
127
|
-
"test/**/*.ts?(x)"
|
|
128
|
-
],
|
|
129
|
-
"rules": {
|
|
130
|
-
"@typescript-eslint/ban-ts-comment": "off",
|
|
131
|
-
"no-console": "off",
|
|
132
|
-
"testing-library/no-container": "off",
|
|
133
|
-
"testing-library/no-node-access": "off"
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
]
|
|
137
|
-
},
|
|
138
122
|
"prettier": "@gilbarbara/prettier-config",
|
|
139
123
|
"size-limit": [
|
|
140
124
|
{
|
package/src/index.tsx
CHANGED
|
@@ -1,251 +1,45 @@
|
|
|
1
|
-
import
|
|
2
|
-
cloneElement,
|
|
3
|
-
isValidElement,
|
|
4
|
-
ReactElement,
|
|
5
|
-
useCallback,
|
|
6
|
-
useEffect,
|
|
7
|
-
useReducer,
|
|
8
|
-
useRef,
|
|
9
|
-
useState,
|
|
10
|
-
} from 'react';
|
|
11
|
-
import convert from 'react-from-dom';
|
|
1
|
+
import { cloneElement, ReactElement, SVGProps } from 'react';
|
|
12
2
|
|
|
13
3
|
import { STATUS } from './config';
|
|
14
4
|
import CacheStore from './modules/cache';
|
|
15
|
-
import { canUseDOM,
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
5
|
+
import { canUseDOM, omit } from './modules/helpers';
|
|
6
|
+
import useInlineSVG from './modules/useInlineSVG';
|
|
7
|
+
import { useCacheStore } from './provider';
|
|
8
|
+
import { Props, Status } from './types';
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
export let cacheStore: CacheStore;
|
|
10
|
+
export const cacheStore = new CacheStore();
|
|
22
11
|
|
|
23
|
-
function
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
description,
|
|
28
|
-
fetchOptions,
|
|
29
|
-
innerRef,
|
|
30
|
-
loader = null,
|
|
31
|
-
onError,
|
|
32
|
-
onLoad,
|
|
33
|
-
src,
|
|
34
|
-
title,
|
|
35
|
-
uniqueHash,
|
|
36
|
-
} = props;
|
|
37
|
-
const [state, setState] = useReducer(
|
|
38
|
-
(previousState: State, nextState: Partial<State>) => ({
|
|
39
|
-
...previousState,
|
|
40
|
-
...nextState,
|
|
41
|
-
}),
|
|
42
|
-
{
|
|
43
|
-
content: '',
|
|
44
|
-
element: null,
|
|
45
|
-
|
|
46
|
-
isCached: cacheRequests && cacheStore.isCached(props.src),
|
|
47
|
-
status: STATUS.IDLE,
|
|
48
|
-
},
|
|
49
|
-
);
|
|
50
|
-
const { content, element, isCached, status } = state;
|
|
51
|
-
const previousProps = usePrevious(props);
|
|
52
|
-
const previousState = usePrevious(state);
|
|
53
|
-
|
|
54
|
-
const hash = useRef(uniqueHash ?? randomString(8));
|
|
55
|
-
const isActive = useRef(false);
|
|
56
|
-
const isInitialized = useRef(false);
|
|
57
|
-
|
|
58
|
-
const handleError = useCallback(
|
|
59
|
-
(error: Error | FetchError) => {
|
|
60
|
-
if (isActive.current) {
|
|
61
|
-
setState({
|
|
62
|
-
status:
|
|
63
|
-
error.message === 'Browser does not support SVG' ? STATUS.UNSUPPORTED : STATUS.FAILED,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
onError?.(error);
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
[onError],
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const handleLoad = useCallback((loadedContent: string, hasCache = false) => {
|
|
73
|
-
if (isActive.current) {
|
|
74
|
-
setState({
|
|
75
|
-
content: loadedContent,
|
|
76
|
-
isCached: hasCache,
|
|
77
|
-
status: STATUS.LOADED,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}, []);
|
|
81
|
-
|
|
82
|
-
const fetchContent = useCallback(async () => {
|
|
83
|
-
const responseContent: string = await request(src, fetchOptions);
|
|
84
|
-
|
|
85
|
-
handleLoad(responseContent);
|
|
86
|
-
}, [fetchOptions, handleLoad, src]);
|
|
87
|
-
|
|
88
|
-
const getElement = useCallback(() => {
|
|
89
|
-
try {
|
|
90
|
-
const node = getNode({ ...props, handleError, hash: hash.current, content }) as Node;
|
|
91
|
-
const convertedElement = convert(node);
|
|
92
|
-
|
|
93
|
-
if (!convertedElement || !isValidElement(convertedElement)) {
|
|
94
|
-
throw new Error('Could not convert the src to a React element');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
setState({
|
|
98
|
-
element: convertedElement,
|
|
99
|
-
status: STATUS.READY,
|
|
100
|
-
});
|
|
101
|
-
} catch (error: any) {
|
|
102
|
-
handleError(new Error(error.message));
|
|
103
|
-
}
|
|
104
|
-
}, [content, handleError, props]);
|
|
105
|
-
|
|
106
|
-
const getContent = useCallback(async () => {
|
|
107
|
-
const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/u.exec(src);
|
|
108
|
-
let inlineSrc;
|
|
109
|
-
|
|
110
|
-
if (dataURI) {
|
|
111
|
-
inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
|
|
112
|
-
} else if (src.includes('<svg')) {
|
|
113
|
-
inlineSrc = src;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (inlineSrc) {
|
|
117
|
-
handleLoad(inlineSrc);
|
|
118
|
-
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
if (cacheRequests) {
|
|
124
|
-
const cachedContent = await cacheStore.get(src, fetchOptions);
|
|
125
|
-
|
|
126
|
-
handleLoad(cachedContent, true);
|
|
127
|
-
} else {
|
|
128
|
-
await fetchContent();
|
|
129
|
-
}
|
|
130
|
-
} catch (error: any) {
|
|
131
|
-
handleError(error);
|
|
132
|
-
}
|
|
133
|
-
}, [cacheRequests, fetchContent, fetchOptions, handleError, handleLoad, src]);
|
|
134
|
-
|
|
135
|
-
const load = useCallback(async () => {
|
|
136
|
-
if (isActive.current) {
|
|
137
|
-
setState({
|
|
138
|
-
content: '',
|
|
139
|
-
element: null,
|
|
140
|
-
isCached: false,
|
|
141
|
-
status: STATUS.LOADING,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}, []);
|
|
145
|
-
|
|
146
|
-
// Run on mount
|
|
147
|
-
useEffect(
|
|
148
|
-
() => {
|
|
149
|
-
isActive.current = true;
|
|
150
|
-
|
|
151
|
-
if (!canUseDOM() || isInitialized.current) {
|
|
152
|
-
return () => undefined;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
if (status === STATUS.IDLE) {
|
|
157
|
-
if (!isSupportedEnvironment()) {
|
|
158
|
-
throw new Error('Browser does not support SVG');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (!src) {
|
|
162
|
-
throw new Error('Missing src');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
load();
|
|
166
|
-
}
|
|
167
|
-
} catch (error: any) {
|
|
168
|
-
handleError(error);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
isInitialized.current = true;
|
|
172
|
-
|
|
173
|
-
return () => {
|
|
174
|
-
isActive.current = false;
|
|
175
|
-
};
|
|
176
|
-
},
|
|
177
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
178
|
-
[],
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
// Handle prop changes
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
if (!canUseDOM()) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (!previousProps) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (previousProps.src !== src) {
|
|
192
|
-
if (!src) {
|
|
193
|
-
handleError(new Error('Missing src'));
|
|
194
|
-
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
load();
|
|
199
|
-
} else if (previousProps.title !== title || previousProps.description !== description) {
|
|
200
|
-
getElement();
|
|
201
|
-
}
|
|
202
|
-
}, [description, getElement, handleError, load, previousProps, src, title]);
|
|
203
|
-
|
|
204
|
-
// handle state
|
|
205
|
-
useEffect(() => {
|
|
206
|
-
if (!previousState) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (previousState.status !== STATUS.LOADING && status === STATUS.LOADING) {
|
|
211
|
-
getContent();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (previousState.status !== STATUS.LOADED && status === STATUS.LOADED) {
|
|
215
|
-
getElement();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (previousState.status !== STATUS.READY && status === STATUS.READY) {
|
|
219
|
-
onLoad?.(src, isCached);
|
|
220
|
-
}
|
|
221
|
-
}, [getContent, getElement, isCached, onLoad, previousState, src, status]);
|
|
12
|
+
export default function InlineSVG(props: Props) {
|
|
13
|
+
const { children = null, innerRef, loader = null } = props;
|
|
14
|
+
const contextStore = useCacheStore();
|
|
15
|
+
const store = contextStore ?? cacheStore;
|
|
222
16
|
|
|
223
|
-
const
|
|
224
|
-
props,
|
|
225
|
-
'baseURL',
|
|
226
|
-
'cacheRequests',
|
|
227
|
-
'children',
|
|
228
|
-
'description',
|
|
229
|
-
'fetchOptions',
|
|
230
|
-
'innerRef',
|
|
231
|
-
'loader',
|
|
232
|
-
'onError',
|
|
233
|
-
'onLoad',
|
|
234
|
-
'preProcessor',
|
|
235
|
-
'src',
|
|
236
|
-
'title',
|
|
237
|
-
'uniqueHash',
|
|
238
|
-
'uniquifyIDs',
|
|
239
|
-
);
|
|
17
|
+
const { element, status } = useInlineSVG(props, store);
|
|
240
18
|
|
|
241
19
|
if (!canUseDOM()) {
|
|
242
20
|
return loader;
|
|
243
21
|
}
|
|
244
22
|
|
|
245
23
|
if (element) {
|
|
246
|
-
return cloneElement(element as ReactElement<
|
|
24
|
+
return cloneElement(element as ReactElement<SVGProps<SVGElement>>, {
|
|
247
25
|
ref: innerRef,
|
|
248
|
-
...
|
|
26
|
+
...omit(
|
|
27
|
+
props,
|
|
28
|
+
'baseURL',
|
|
29
|
+
'cacheRequests',
|
|
30
|
+
'children',
|
|
31
|
+
'description',
|
|
32
|
+
'fetchOptions',
|
|
33
|
+
'innerRef',
|
|
34
|
+
'loader',
|
|
35
|
+
'onError',
|
|
36
|
+
'onLoad',
|
|
37
|
+
'preProcessor',
|
|
38
|
+
'src',
|
|
39
|
+
'title',
|
|
40
|
+
'uniqueHash',
|
|
41
|
+
'uniquifyIDs',
|
|
42
|
+
),
|
|
249
43
|
});
|
|
250
44
|
}
|
|
251
45
|
|
|
@@ -256,30 +50,4 @@ function ReactInlineSVG(props: Props) {
|
|
|
256
50
|
return loader;
|
|
257
51
|
}
|
|
258
52
|
|
|
259
|
-
export default function InlineSVG(props: Props) {
|
|
260
|
-
if (!cacheStore) {
|
|
261
|
-
cacheStore = new CacheStore();
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const { loader } = props;
|
|
265
|
-
const hasCallback = useRef(false);
|
|
266
|
-
const [isReady, setReady] = useState(cacheStore.isReady);
|
|
267
|
-
|
|
268
|
-
useEffect(() => {
|
|
269
|
-
if (!hasCallback.current) {
|
|
270
|
-
cacheStore.onReady(() => {
|
|
271
|
-
setReady(true);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
hasCallback.current = true;
|
|
275
|
-
}
|
|
276
|
-
}, []);
|
|
277
|
-
|
|
278
|
-
if (!isReady) {
|
|
279
|
-
return loader;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return <ReactInlineSVG {...props} />;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
53
|
export * from './types';
|