1 | import * as React from 'react'
|
2 | import invariant from 'invariant'
|
3 |
|
4 | import { injectScript } from './utils/injectscript'
|
5 | import { preventGoogleFonts } from './utils/prevent-google-fonts'
|
6 |
|
7 | import { isBrowser } from './utils/isbrowser'
|
8 | import { LoadScriptUrlOptions, makeLoadScriptUrl } from './utils/make-load-script-url'
|
9 |
|
10 | let cleaningUp = false
|
11 |
|
12 | interface LoadScriptState {
|
13 | loaded: boolean
|
14 | }
|
15 |
|
16 | export 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 |
|
25 | export function DefaultLoadingElement(): JSX.Element {
|
26 | return <div>{`Loading...`}</div>
|
27 | }
|
28 |
|
29 | export const defaultLoadScriptProps = {
|
30 | id: 'script-loader',
|
31 | version: 'weekly',
|
32 | }
|
33 |
|
34 | class 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 error(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 |
|
75 |
|
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 |
|
226 | export default LoadScript
|