UNPKG

7.29 kBJavaScriptView Raw
1import React from "react"
2import PropTypes from "prop-types"
3import loader from "./loader"
4import redirects from "./redirects.json"
5import { apiRunner } from "./api-runner-browser"
6import emitter from "./emitter"
7import { navigate as reachNavigate } from "@reach/router"
8import { parsePath } from "gatsby-link"
9
10// Convert to a map for faster lookup in maybeRedirect()
11const redirectMap = redirects.reduce((map, redirect) => {
12 map[redirect.fromPath] = redirect
13 return map
14}, {})
15
16function maybeRedirect(pathname) {
17 const redirect = redirectMap[pathname]
18
19 if (redirect != null) {
20 if (process.env.NODE_ENV !== `production`) {
21 const pageResources = loader.loadPageSync(pathname)
22
23 if (pageResources != null) {
24 console.error(
25 `The route "${pathname}" matches both a page and a redirect; this is probably not intentional.`
26 )
27 }
28 }
29
30 window.___replace(redirect.toPath)
31 return true
32 } else {
33 return false
34 }
35}
36
37const onPreRouteUpdate = (location, prevLocation) => {
38 if (!maybeRedirect(location.pathname)) {
39 apiRunner(`onPreRouteUpdate`, { location, prevLocation })
40 }
41}
42
43const onRouteUpdate = (location, prevLocation) => {
44 if (!maybeRedirect(location.pathname)) {
45 apiRunner(`onRouteUpdate`, { location, prevLocation })
46 // Temp hack while awaiting https://github.com/reach/router/issues/119
47 window.__navigatingToLink = false
48 }
49}
50
51const navigate = (to, options = {}) => {
52 // Temp hack while awaiting https://github.com/reach/router/issues/119
53 if (!options.replace) {
54 window.__navigatingToLink = true
55 }
56
57 let { pathname } = parsePath(to)
58 const redirect = redirectMap[pathname]
59
60 // If we're redirecting, just replace the passed in pathname
61 // to the one we want to redirect to.
62 if (redirect) {
63 to = redirect.toPath
64 pathname = parsePath(to).pathname
65 }
66
67 // If we had a service worker update, no matter the path, reload window and
68 // reset the pathname whitelist
69 if (window.___swUpdated) {
70 window.location = pathname
71 return
72 }
73
74 // Start a timer to wait for a second before transitioning and showing a
75 // loader in case resources aren't around yet.
76 const timeoutId = setTimeout(() => {
77 emitter.emit(`onDelayedLoadPageResources`, { pathname })
78 apiRunner(`onRouteUpdateDelayed`, {
79 location: window.location,
80 })
81 }, 1000)
82
83 loader.loadPage(pathname).then(pageResources => {
84 // If no page resources, then refresh the page
85 // Do this, rather than simply `window.location.reload()`, so that
86 // pressing the back/forward buttons work - otherwise when pressing
87 // back, the browser will just change the URL and expect JS to handle
88 // the change, which won't always work since it might not be a Gatsby
89 // page.
90 if (!pageResources || pageResources.status === `error`) {
91 window.history.replaceState({}, ``, location.href)
92 window.location = pathname
93 }
94 // If the loaded page has a different compilation hash to the
95 // window, then a rebuild has occurred on the server. Reload.
96 if (process.env.NODE_ENV === `production` && pageResources) {
97 if (
98 pageResources.page.webpackCompilationHash !==
99 window.___webpackCompilationHash
100 ) {
101 // Purge plugin-offline cache
102 if (
103 `serviceWorker` in navigator &&
104 navigator.serviceWorker.controller !== null &&
105 navigator.serviceWorker.controller.state === `activated`
106 ) {
107 navigator.serviceWorker.controller.postMessage({
108 gatsbyApi: `clearPathResources`,
109 })
110 }
111
112 console.log(`Site has changed on server. Reloading browser`)
113 window.location = pathname
114 }
115 }
116 reachNavigate(to, options)
117 clearTimeout(timeoutId)
118 })
119}
120
121function shouldUpdateScroll(prevRouterProps, { location }) {
122 const { pathname, hash } = location
123 const results = apiRunner(`shouldUpdateScroll`, {
124 prevRouterProps,
125 // `pathname` for backwards compatibility
126 pathname,
127 routerProps: { location },
128 getSavedScrollPosition: args => this._stateStorage.read(args),
129 })
130 if (results.length > 0) {
131 // Use the latest registered shouldUpdateScroll result, this allows users to override plugin's configuration
132 // @see https://github.com/gatsbyjs/gatsby/issues/12038
133 return results[results.length - 1]
134 }
135
136 if (prevRouterProps) {
137 const {
138 location: { pathname: oldPathname },
139 } = prevRouterProps
140 if (oldPathname === pathname) {
141 // Scroll to element if it exists, if it doesn't, or no hash is provided,
142 // scroll to top.
143 return hash ? decodeURI(hash.slice(1)) : [0, 0]
144 }
145 }
146 return true
147}
148
149function init() {
150 // Temp hack while awaiting https://github.com/reach/router/issues/119
151 window.__navigatingToLink = false
152
153 window.___push = to => navigate(to, { replace: false })
154 window.___replace = to => navigate(to, { replace: true })
155 window.___navigate = (to, options) => navigate(to, options)
156
157 // Check for initial page-load redirect
158 maybeRedirect(window.location.pathname)
159}
160
161class RouteAnnouncer extends React.Component {
162 constructor(props) {
163 super(props)
164 this.announcementRef = React.createRef()
165 }
166
167 componentDidUpdate(prevProps, nextProps) {
168 requestAnimationFrame(() => {
169 let pageName = `new page at ${this.props.location.pathname}`
170 if (document.title) {
171 pageName = document.title
172 }
173 const pageHeadings = document
174 .getElementById(`gatsby-focus-wrapper`)
175 .getElementsByTagName(`h1`)
176 if (pageHeadings && pageHeadings.length) {
177 pageName = pageHeadings[0].textContent
178 }
179 const newAnnouncement = `Navigated to ${pageName}`
180 const oldAnnouncement = this.announcementRef.current.innerText
181 if (oldAnnouncement !== newAnnouncement) {
182 this.announcementRef.current.innerText = newAnnouncement
183 }
184 })
185 }
186
187 render() {
188 return (
189 <div
190 id="gatsby-announcer"
191 style={{
192 position: `absolute`,
193 width: 1,
194 height: 1,
195 padding: 0,
196 overflow: `hidden`,
197 clip: `rect(0, 0, 0, 0)`,
198 whiteSpace: `nowrap`,
199 border: 0,
200 }}
201 role="alert"
202 aria-live="assertive"
203 aria-atomic="true"
204 ref={this.announcementRef}
205 ></div>
206 )
207 }
208}
209
210// Fire on(Pre)RouteUpdate APIs
211class RouteUpdates extends React.Component {
212 constructor(props) {
213 super(props)
214 onPreRouteUpdate(props.location, null)
215 }
216
217 componentDidMount() {
218 onRouteUpdate(this.props.location, null)
219 }
220
221 componentDidUpdate(prevProps, prevState, shouldFireRouteUpdate) {
222 if (shouldFireRouteUpdate) {
223 onRouteUpdate(this.props.location, prevProps.location)
224 }
225 }
226
227 getSnapshotBeforeUpdate(prevProps) {
228 if (this.props.location.pathname !== prevProps.location.pathname) {
229 onPreRouteUpdate(this.props.location, prevProps.location)
230 return true
231 }
232
233 return false
234 }
235
236 render() {
237 return (
238 <React.Fragment>
239 {this.props.children}
240 <RouteAnnouncer location={location} />
241 </React.Fragment>
242 )
243 }
244}
245
246RouteUpdates.propTypes = {
247 location: PropTypes.object.isRequired,
248}
249
250export { init, shouldUpdateScroll, RouteUpdates }