UNPKG

5.93 kBTypeScriptView Raw
1import * as React from 'react'
2import invariant from 'invariant'
3
4import { injectScript } from './utils/injectscript'
5import { preventGoogleFonts } from './utils/prevent-google-fonts'
6
7import { isBrowser } from './utils/isbrowser'
8import { LoadScriptUrlOptions, makeLoadScriptUrl } from './utils/make-load-script-url'
9
10let cleaningUp = false
11
12interface LoadScriptState {
13 loaded: boolean
14}
15
16export interface LoadScriptProps extends LoadScriptUrlOptions {
17 id: string
18 loadingElement?: React.ReactNode
19 onLoad?: () => void
20 onError?: (error: Error) => void
21 onUnmount?: () => void
22 preventGoogleFontsLoading?: boolean
23}
24
25export function DefaultLoadingElement(): JSX.Element {
26 return <div>{`Loading...`}</div>
27}
28
29export const defaultLoadScriptProps = {
30 id: 'script-loader',
31 version: 'weekly',
32}
33
34class LoadScript extends React.PureComponent<LoadScriptProps, LoadScriptState> {
35 public static defaultProps = defaultLoadScriptProps
36
37 check: React.RefObject<HTMLDivElement> = React.createRef()
38
39 state = {
40 loaded: false,
41 }
42
43 cleanupCallback = (): void => {
44 delete window.google
45
46 this.injectScript()
47 }
48
49 componentDidMount(): void {
50 if (isBrowser) {
51 if (window.google && !cleaningUp) {
52 console.error('google api is already presented')
53
54 return
55 }
56
57 this.isCleaningUp()
58 .then(this.injectScript)
59 .catch(function err(err) {
60 console.error('Error at injecting script after cleaning up: ', err)
61 })
62 }
63 }
64
65 componentDidUpdate(prevProps: LoadScriptProps): void {
66 if (this.props.libraries !== prevProps.libraries) {
67 console.warn(
68 'Performance warning! LoadScript has been reloaded unintentionally! You should not pass `libraries` prop as new array. Please keep an array of libraries as static class property for Components and PureComponents, or just a const variable outside of component, or somewhere in config files or ENV variables'
69 )
70 }
71
72 if (isBrowser && prevProps.language !== this.props.language) {
73 this.cleanup()
74 // TODO: refactor to use gDSFP maybe... wait for hooks refactoring.
75 // eslint-disable-next-line react/no-did-update-set-state
76 this.setState(function setLoaded() {
77 return {
78 loaded: false,
79 }
80 }, this.cleanupCallback)
81 }
82 }
83
84 componentWillUnmount(): void {
85 if (isBrowser) {
86 this.cleanup()
87
88 const timeoutCallback = (): void => {
89 if (!this.check.current) {
90 delete window.google
91 cleaningUp = false
92 }
93 }
94
95 window.setTimeout(timeoutCallback, 1)
96
97 if (this.props.onUnmount) {
98 this.props.onUnmount()
99 }
100 }
101 }
102
103 isCleaningUp = async (): Promise<void> => {
104 function promiseCallback(resolve: () => void): void {
105 if (!cleaningUp) {
106 resolve()
107 } else {
108 if (isBrowser) {
109 const timer = window.setInterval(function interval() {
110 if (!cleaningUp) {
111 window.clearInterval(timer)
112
113 resolve()
114 }
115 }, 1)
116 }
117 }
118
119 return
120 }
121
122 return new Promise(promiseCallback)
123 }
124
125 cleanup = (): void => {
126 cleaningUp = true
127 const script = document.getElementById(this.props.id)
128
129 if (script && script.parentNode) {
130 script.parentNode.removeChild(script)
131 }
132
133 Array.prototype.slice
134 .call(document.getElementsByTagName('script'))
135 .filter(function filter(script: HTMLScriptElement): boolean {
136 return script.src.includes('maps.googleapis')
137 })
138 .forEach(function forEach(script: HTMLScriptElement): void {
139 if (script.parentNode) {
140 script.parentNode.removeChild(script)
141 }
142 })
143
144 Array.prototype.slice
145 .call(document.getElementsByTagName('link'))
146 .filter(function filter(link: HTMLLinkElement): boolean {
147 return (
148 link.href === 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Google+Sans'
149 )
150 })
151 .forEach(function forEach(link: HTMLLinkElement) {
152 if (link.parentNode) {
153 link.parentNode.removeChild(link)
154 }
155 })
156
157 Array.prototype.slice
158 .call(document.getElementsByTagName('style'))
159 .filter(function filter(style: HTMLStyleElement): boolean {
160 return (
161 style.innerText !== undefined &&
162 style.innerText.length > 0 &&
163 style.innerText.includes('.gm-')
164 )
165 })
166 .forEach(function forEach(style: HTMLStyleElement) {
167 if (style.parentNode) {
168 style.parentNode.removeChild(style)
169 }
170 })
171 }
172
173 injectScript = (): void => {
174 if (this.props.preventGoogleFontsLoading) {
175 preventGoogleFonts()
176 }
177
178 invariant(!!this.props.id, 'LoadScript requires "id" prop to be a string: %s', this.props.id)
179
180 const injectScriptOptions = {
181 id: this.props.id,
182 url: makeLoadScriptUrl(this.props),
183 }
184
185 injectScript(injectScriptOptions)
186 .then(() => {
187 if (this.props.onLoad) {
188 this.props.onLoad()
189 }
190
191 this.setState(function setLoaded() {
192 return {
193 loaded: true,
194 }
195 })
196
197 return
198 })
199 .catch(err => {
200 if (this.props.onError) {
201 this.props.onError(err)
202 }
203
204 console.error(`
205 There has been an Error with loading Google Maps API script, please check that you provided correct google API key (${this
206 .props.googleMapsApiKey || '-'}) or Client ID (${this.props.googleMapsClientId ||
207 '-'}) to <LoadScript />
208 Otherwise it is a Network issue.
209 `)
210 })
211 }
212
213 render(): React.ReactNode {
214 return (
215 <>
216 <div ref={this.check} />
217
218 {this.state.loaded
219 ? this.props.children
220 : this.props.loadingElement || <DefaultLoadingElement />}
221 </>
222 )
223 }
224}
225
226export default LoadScript