react-inlinesvg 4.0.6 → 4.1.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/src/index.tsx CHANGED
@@ -1,331 +1,256 @@
1
- import * as React from 'react';
1
+ import {
2
+ cloneElement,
3
+ isValidElement,
4
+ ReactElement,
5
+ useCallback,
6
+ useEffect,
7
+ useReducer,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
2
11
  import convert from 'react-from-dom';
3
12
 
4
- import CacheStore from './cache';
5
13
  import { STATUS } from './config';
6
- import { canUseDOM, isSupportedEnvironment, omit, randomString, request } from './helpers';
14
+ import CacheStore from './modules/cache';
15
+ import { canUseDOM, isSupportedEnvironment, omit, randomString, request } from './modules/helpers';
16
+ import { usePrevious } from './modules/hooks';
17
+ import { getNode } from './modules/utils';
7
18
  import { FetchError, Props, State, Status } from './types';
8
19
 
9
20
  // eslint-disable-next-line import/no-mutable-exports
10
21
  export let cacheStore: CacheStore;
11
22
 
12
- class ReactInlineSVG extends React.PureComponent<Props, State> {
13
- private readonly hash: string;
14
- private isActive = false;
15
- private isInitialized = false;
16
-
17
- public static defaultProps = {
18
- cacheRequests: true,
19
- uniquifyIDs: false,
20
- };
21
-
22
- constructor(props: Props) {
23
- super(props);
24
-
25
- this.state = {
23
+ function ReactInlineSVG(props: Props) {
24
+ const {
25
+ cacheRequests = true,
26
+ children = null,
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
+ {
26
43
  content: '',
27
44
  element: null,
28
- isCached: !!props.cacheRequests && cacheStore.isCached(props.src),
29
- status: STATUS.IDLE,
30
- };
31
-
32
- this.hash = props.uniqueHash ?? randomString(8);
33
- }
34
-
35
- public componentDidMount(): void {
36
- this.isActive = true;
37
-
38
- if (!canUseDOM() || this.isInitialized) {
39
- return;
40
- }
41
-
42
- const { status } = this.state;
43
- const { src } = this.props;
44
-
45
- try {
46
- if (status === STATUS.IDLE) {
47
- if (!isSupportedEnvironment()) {
48
- throw new Error('Browser does not support SVG');
49
- }
50
-
51
- if (!src) {
52
- throw new Error('Missing src');
53
- }
54
-
55
- this.load();
56
- }
57
- } catch (error: any) {
58
- this.handleError(error);
59
- }
60
-
61
- this.isInitialized = true;
62
- }
63
-
64
- public componentDidUpdate(previousProps: Props, previousState: State): void {
65
- if (!canUseDOM()) {
66
- return;
67
- }
68
-
69
- const { isCached, status } = this.state;
70
- const { description, onLoad, src, title } = this.props;
71
45
 
72
- if (previousState.status !== STATUS.READY && status === STATUS.READY) {
73
- if (onLoad) {
74
- onLoad(src, isCached);
75
- }
76
- }
77
-
78
- if (previousProps.src !== src) {
79
- if (!src) {
80
- this.handleError(new Error('Missing src'));
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
+ });
81
65
 
82
- return;
66
+ onError?.(error);
83
67
  }
84
-
85
- this.load();
86
- }
87
-
88
- if (previousProps.title !== title || previousProps.description !== description) {
89
- this.getElement();
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
+ });
90
79
  }
91
- }
92
-
93
- public componentWillUnmount(): void {
94
- this.isActive = false;
95
- }
96
-
97
- private fetchContent = async () => {
98
- const { fetchOptions, src } = this.props;
80
+ }, []);
99
81
 
100
- const content: string = await request(src, fetchOptions);
82
+ const fetchContent = useCallback(async () => {
83
+ const responseContent: string = await request(src, fetchOptions);
101
84
 
102
- this.handleLoad(content);
103
- };
85
+ handleLoad(responseContent);
86
+ }, [fetchOptions, handleLoad, src]);
104
87
 
105
- private getElement() {
88
+ const getElement = useCallback(() => {
106
89
  try {
107
- const node = this.getNode() as Node;
108
- const element = convert(node);
90
+ const node = getNode({ ...props, handleError, hash: hash.current, content }) as Node;
91
+ const convertedElement = convert(node);
109
92
 
110
- if (!element || !React.isValidElement(element)) {
93
+ if (!convertedElement || !isValidElement(convertedElement)) {
111
94
  throw new Error('Could not convert the src to a React element');
112
95
  }
113
96
 
114
- this.setState({
115
- element,
97
+ setState({
98
+ element: convertedElement,
116
99
  status: STATUS.READY,
117
100
  });
118
101
  } catch (error: any) {
119
- this.handleError(new Error(error.message));
102
+ handleError(new Error(error.message));
120
103
  }
121
- }
122
-
123
- private getNode() {
124
- const { description, title } = this.props;
125
-
126
- try {
127
- const svgText = this.processSVG();
128
- const node = convert(svgText, { nodeOnly: true });
129
-
130
- if (!node || !(node instanceof SVGSVGElement)) {
131
- throw new Error('Could not convert the src to a DOM Node');
132
- }
133
-
134
- const svg = this.updateSVGAttributes(node);
135
-
136
- if (description) {
137
- const originalDesc = svg.querySelector('desc');
138
-
139
- if (originalDesc?.parentNode) {
140
- originalDesc.parentNode.removeChild(originalDesc);
141
- }
104
+ }, [content, handleError, props]);
142
105
 
143
- const descElement = document.createElementNS('http://www.w3.org/2000/svg', 'desc');
106
+ const getContent = useCallback(async () => {
107
+ const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/u.exec(src);
108
+ let inlineSrc;
144
109
 
145
- descElement.innerHTML = description;
146
- svg.prepend(descElement);
147
- }
110
+ if (dataURI) {
111
+ inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
112
+ } else if (src.includes('<svg')) {
113
+ inlineSrc = src;
114
+ }
148
115
 
149
- if (typeof title !== 'undefined') {
150
- const originalTitle = svg.querySelector('title');
116
+ if (inlineSrc) {
117
+ handleLoad(inlineSrc);
151
118
 
152
- if (originalTitle?.parentNode) {
153
- originalTitle.parentNode.removeChild(originalTitle);
154
- }
119
+ return;
120
+ }
155
121
 
156
- if (title) {
157
- const titleElement = document.createElementNS('http://www.w3.org/2000/svg', 'title');
122
+ try {
123
+ if (cacheRequests) {
124
+ const cachedContent = await cacheStore.get(src, fetchOptions);
158
125
 
159
- titleElement.innerHTML = title;
160
- svg.prepend(titleElement);
161
- }
126
+ handleLoad(cachedContent, true);
127
+ } else {
128
+ await fetchContent();
162
129
  }
163
-
164
- return svg;
165
130
  } catch (error: any) {
166
- return this.handleError(error);
131
+ handleError(error);
167
132
  }
168
- }
169
-
170
- private handleError = (error: Error | FetchError) => {
171
- const { onError } = this.props;
172
- const status =
173
- error.message === 'Browser does not support SVG' ? STATUS.UNSUPPORTED : STATUS.FAILED;
174
-
175
- if (this.isActive) {
176
- this.setState({ status }, () => {
177
- if (typeof onError === 'function') {
178
- onError(error);
179
- }
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,
180
142
  });
181
143
  }
182
- };
183
-
184
- private handleLoad = (content: string, hasCache = false) => {
185
- if (this.isActive) {
186
- this.setState(
187
- {
188
- content,
189
- isCached: hasCache,
190
- status: STATUS.LOADED,
191
- },
192
- this.getElement,
193
- );
194
- }
195
- };
196
-
197
- private load() {
198
- if (this.isActive) {
199
- this.setState(
200
- {
201
- content: '',
202
- element: null,
203
- isCached: false,
204
- status: STATUS.LOADING,
205
- },
206
- async () => {
207
- const { cacheRequests, fetchOptions, src } = this.props;
208
-
209
- const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/u.exec(src);
210
- let inlineSrc;
211
-
212
- if (dataURI) {
213
- inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
214
- } else if (src.includes('<svg')) {
215
- inlineSrc = src;
216
- }
217
-
218
- if (inlineSrc) {
219
- this.handleLoad(inlineSrc);
144
+ }, []);
220
145
 
221
- return;
222
- }
146
+ // Run on mount
147
+ useEffect(
148
+ () => {
149
+ isActive.current = true;
223
150
 
224
- try {
225
- if (cacheRequests) {
226
- const content = await cacheStore.get(src, fetchOptions);
151
+ if (!canUseDOM() || isInitialized.current) {
152
+ return () => undefined;
153
+ }
227
154
 
228
- this.handleLoad(content, true);
229
- } else {
230
- await this.fetchContent();
231
- }
232
- } catch (error: any) {
233
- this.handleError(error);
155
+ try {
156
+ if (status === STATUS.IDLE) {
157
+ if (!isSupportedEnvironment()) {
158
+ throw new Error('Browser does not support SVG');
234
159
  }
235
- },
236
- );
237
- }
238
- }
239
160
 
240
- private processSVG() {
241
- const { content } = this.state;
242
- const { preProcessor } = this.props;
161
+ if (!src) {
162
+ throw new Error('Missing src');
163
+ }
243
164
 
244
- if (preProcessor) {
245
- return preProcessor(content);
246
- }
165
+ load();
166
+ }
167
+ } catch (error: any) {
168
+ handleError(error);
169
+ }
247
170
 
248
- return content;
249
- }
171
+ isInitialized.current = true;
250
172
 
251
- private updateSVGAttributes(node: SVGSVGElement): SVGSVGElement {
252
- const { baseURL = '', uniquifyIDs } = this.props;
253
- const replaceableAttributes = ['id', 'href', 'xlink:href', 'xlink:role', 'xlink:arcrole'];
254
- const linkAttributes = ['href', 'xlink:href'];
255
- const isDataValue = (name: string, value: string) =>
256
- linkAttributes.includes(name) && (value ? !value.includes('#') : false);
173
+ return () => {
174
+ isActive.current = false;
175
+ };
176
+ },
177
+ // eslint-disable-next-line react-hooks/exhaustive-deps
178
+ [],
179
+ );
257
180
 
258
- if (!uniquifyIDs) {
259
- return node;
181
+ // Handle prop changes
182
+ useEffect(() => {
183
+ if (!canUseDOM()) {
184
+ return;
260
185
  }
261
186
 
262
- [...node.children].forEach(d => {
263
- if (d.attributes?.length) {
264
- const attributes = Object.values(d.attributes).map(a => {
265
- const attribute = a;
266
- const match = /url\((.*?)\)/.exec(a.value);
267
-
268
- if (match?.[1]) {
269
- attribute.value = a.value.replace(match[0], `url(${baseURL}${match[1]}__${this.hash})`);
270
- }
271
-
272
- return attribute;
273
- });
274
-
275
- replaceableAttributes.forEach(r => {
276
- const attribute = attributes.find(a => a.name === r);
187
+ if (!previousProps) {
188
+ return;
189
+ }
277
190
 
278
- if (attribute && !isDataValue(r, attribute.value)) {
279
- attribute.value = `${attribute.value}__${this.hash}`;
280
- }
281
- });
282
- }
191
+ if (previousProps.src !== src) {
192
+ if (!src) {
193
+ handleError(new Error('Missing src'));
283
194
 
284
- if (d.children.length) {
285
- return this.updateSVGAttributes(d as SVGSVGElement);
195
+ return;
286
196
  }
287
197
 
288
- return d;
289
- });
290
-
291
- return node;
292
- }
293
-
294
- public render(): React.ReactNode {
295
- const { element, status } = this.state;
296
- const { children = null, innerRef, loader = null } = this.props;
297
- const elementProps = omit(
298
- this.props,
299
- 'baseURL',
300
- 'cacheRequests',
301
- 'children',
302
- 'description',
303
- 'fetchOptions',
304
- 'innerRef',
305
- 'loader',
306
- 'onError',
307
- 'onLoad',
308
- 'preProcessor',
309
- 'src',
310
- 'title',
311
- 'uniqueHash',
312
- 'uniquifyIDs',
313
- );
198
+ load();
199
+ } else if (previousProps.title !== title || previousProps.description !== description) {
200
+ getElement();
201
+ }
202
+ }, [description, getElement, handleError, load, previousProps, src, title]);
314
203
 
315
- if (!canUseDOM()) {
316
- return loader;
204
+ // handle state
205
+ useEffect(() => {
206
+ if (!previousState) {
207
+ return;
317
208
  }
318
209
 
319
- if (element) {
320
- return React.cloneElement(element as React.ReactElement, { ref: innerRef, ...elementProps });
210
+ if (previousState.status !== STATUS.LOADING && status === STATUS.LOADING) {
211
+ getContent();
321
212
  }
322
213
 
323
- if (([STATUS.UNSUPPORTED, STATUS.FAILED] as Status[]).includes(status)) {
324
- return children;
214
+ if (previousState.status !== STATUS.LOADED && status === STATUS.LOADED) {
215
+ getElement();
325
216
  }
326
217
 
218
+ if (previousState.status !== STATUS.READY && status === STATUS.READY) {
219
+ onLoad?.(src, isCached);
220
+ }
221
+ }, [getContent, getElement, isCached, onLoad, previousState, src, status]);
222
+
223
+ const elementProps = omit(
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
+ );
240
+
241
+ if (!canUseDOM()) {
327
242
  return loader;
328
243
  }
244
+
245
+ if (element) {
246
+ return cloneElement(element as ReactElement, { ref: innerRef, ...elementProps });
247
+ }
248
+
249
+ if (([STATUS.UNSUPPORTED, STATUS.FAILED] as Status[]).includes(status)) {
250
+ return children;
251
+ }
252
+
253
+ return loader;
329
254
  }
330
255
 
331
256
  export default function InlineSVG(props: Props) {
@@ -334,10 +259,10 @@ export default function InlineSVG(props: Props) {
334
259
  }
335
260
 
336
261
  const { loader } = props;
337
- const hasCallback = React.useRef(false);
338
- const [isReady, setReady] = React.useState(cacheStore.isReady);
262
+ const hasCallback = useRef(false);
263
+ const [isReady, setReady] = useState(cacheStore.isReady);
339
264
 
340
- React.useEffect(() => {
265
+ useEffect(() => {
341
266
  if (!hasCallback.current) {
342
267
  cacheStore.onReady(() => {
343
268
  setReady(true);
@@ -1,6 +1,7 @@
1
- import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from './config';
2
1
  import { canUseDOM, request, sleep } from './helpers';
3
- import { StorageItem } from './types';
2
+
3
+ import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from '../config';
4
+ import { StorageItem } from '../types';
4
5
 
5
6
  export default class CacheStore {
6
7
  private cacheApi: Cache | undefined;
@@ -1,4 +1,4 @@
1
- import type { PlainObject } from './types';
1
+ import type { PlainObject } from '../types';
2
2
 
3
3
  export function canUseDOM(): boolean {
4
4
  return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
@@ -0,0 +1,11 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ export function usePrevious<T>(state: T): T | undefined {
4
+ const ref = useRef<T>();
5
+
6
+ useEffect(() => {
7
+ ref.current = state;
8
+ });
9
+
10
+ return ref.current;
11
+ }
@@ -0,0 +1,122 @@
1
+ import convert from 'react-from-dom';
2
+
3
+ import { Props, State } from '../types';
4
+
5
+ interface GetNodeOptions extends Props, Pick<State, 'content'> {
6
+ handleError: (error: Error) => void;
7
+ hash: string;
8
+ }
9
+
10
+ interface UpdateSVGAttributesOptions extends Pick<Props, 'baseURL' | 'uniquifyIDs'> {
11
+ hash: string;
12
+ }
13
+
14
+ export function getNode(options: GetNodeOptions) {
15
+ const {
16
+ baseURL,
17
+ content,
18
+ description,
19
+ handleError,
20
+ hash,
21
+ preProcessor,
22
+ title,
23
+ uniquifyIDs = false,
24
+ } = options;
25
+
26
+ try {
27
+ const svgText = processSVG(content, preProcessor);
28
+ const node = convert(svgText, { nodeOnly: true });
29
+
30
+ if (!node || !(node instanceof SVGSVGElement)) {
31
+ throw new Error('Could not convert the src to a DOM Node');
32
+ }
33
+
34
+ const svg = updateSVGAttributes(node, { baseURL, hash, uniquifyIDs });
35
+
36
+ if (description) {
37
+ const originalDesc = svg.querySelector('desc');
38
+
39
+ if (originalDesc?.parentNode) {
40
+ originalDesc.parentNode.removeChild(originalDesc);
41
+ }
42
+
43
+ const descElement = document.createElementNS('http://www.w3.org/2000/svg', 'desc');
44
+
45
+ descElement.innerHTML = description;
46
+ svg.prepend(descElement);
47
+ }
48
+
49
+ if (typeof title !== 'undefined') {
50
+ const originalTitle = svg.querySelector('title');
51
+
52
+ if (originalTitle?.parentNode) {
53
+ originalTitle.parentNode.removeChild(originalTitle);
54
+ }
55
+
56
+ if (title) {
57
+ const titleElement = document.createElementNS('http://www.w3.org/2000/svg', 'title');
58
+
59
+ titleElement.innerHTML = title;
60
+ svg.prepend(titleElement);
61
+ }
62
+ }
63
+
64
+ return svg;
65
+ } catch (error: any) {
66
+ return handleError(error);
67
+ }
68
+ }
69
+
70
+ export function processSVG(content: string, preProcessor?: Props['preProcessor']) {
71
+ if (preProcessor) {
72
+ return preProcessor(content);
73
+ }
74
+
75
+ return content;
76
+ }
77
+
78
+ export function updateSVGAttributes(
79
+ node: SVGSVGElement,
80
+ options: UpdateSVGAttributesOptions,
81
+ ): SVGSVGElement {
82
+ const { baseURL = '', hash, uniquifyIDs } = options;
83
+ const replaceableAttributes = ['id', 'href', 'xlink:href', 'xlink:role', 'xlink:arcrole'];
84
+ const linkAttributes = ['href', 'xlink:href'];
85
+ const isDataValue = (name: string, value: string) =>
86
+ linkAttributes.includes(name) && (value ? !value.includes('#') : false);
87
+
88
+ if (!uniquifyIDs) {
89
+ return node;
90
+ }
91
+
92
+ [...node.children].forEach(d => {
93
+ if (d.attributes?.length) {
94
+ const attributes = Object.values(d.attributes).map(a => {
95
+ const attribute = a;
96
+ const match = /url\((.*?)\)/.exec(a.value);
97
+
98
+ if (match?.[1]) {
99
+ attribute.value = a.value.replace(match[0], `url(${baseURL}${match[1]}__${hash})`);
100
+ }
101
+
102
+ return attribute;
103
+ });
104
+
105
+ replaceableAttributes.forEach(r => {
106
+ const attribute = attributes.find(a => a.name === r);
107
+
108
+ if (attribute && !isDataValue(r, attribute.value)) {
109
+ attribute.value = `${attribute.value}__${hash}`;
110
+ }
111
+ });
112
+ }
113
+
114
+ if (d.children.length) {
115
+ return updateSVGAttributes(d as SVGSVGElement, options);
116
+ }
117
+
118
+ return d;
119
+ });
120
+
121
+ return node;
122
+ }