mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-03 19:32:35 +02:00
feat(v2): Plugin for Offline/PWA support (#2205)
* implement PWA plugin * added pwa support for docusaurus website * moved sw registration to client module * moved compile function to webpack util * build sw using webpack and render pwa popup * implement @theme/PwaReloadPopup * update website sw to use modules * updated pwa readme * fix header lint errors * apply code formatting * cache files only for mobile, saveData, or installed pwa * added comments about clearing registrations * fixed prettier error * updated pwa README * fix README JS * move /blog => /blog/index.html logic to else branch * add `alwaysPrecache` option * updated docusaurus-plugin-pwa version * added pwa to using-plugins.md * review fixes * re-disable restricted-globals to use self in service worker * useless doc * Update packages/docusaurus-plugin-pwa/README.md Co-authored-by: Reece Dunham <me@rdil.rocks> * Update packages/docusaurus-plugin-pwa/README.md * update a bit pwa doc + minor refactors * minor refactors + add workbox debug mode * env PWA_ prefix * typo * minor refactor * fix file output * add serve:v2:ssl yarn command * minor pwa fixes * typo * add dynamic import comment in SW * comment * let the PWA plugin implement its reload popup on his own * pwa: add Joi options validation * pwa plugin should have its own webpack/babel custom setup * PWA: - debug logs - better SW params system - offline mode activation strategies - docs * add pwa install gif * pwa: popup -> reloadPopup + minor refactors * fix process.env reading + better debug log * minor fixes * minor changes * minor changes Co-authored-by: slorber <lorber.sebastien@gmail.com> Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com> Co-authored-by: Reece Dunham <me@rdil.rocks>
This commit is contained in:
parent
46f794b2ba
commit
9b3da59886
29 changed files with 1508 additions and 45 deletions
173
packages/docusaurus-plugin-pwa/src/index.js
Normal file
173
packages/docusaurus-plugin-pwa/src/index.js
Normal file
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const LogPlugin = require('@docusaurus/core/lib/webpack/plugins/LogPlugin');
|
||||
const {compile} = require('@docusaurus/core/lib/webpack/utils');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const {injectManifest} = require('workbox-build');
|
||||
const {PluginOptionSchema} = require('./pluginOptionSchema');
|
||||
const Terser = require('terser-webpack-plugin');
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
function getSWBabelLoader() {
|
||||
return {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
presets: [
|
||||
[
|
||||
require.resolve('@babel/preset-env'),
|
||||
{
|
||||
useBuiltIns: 'usage',
|
||||
corejs: '2',
|
||||
// See https://twitter.com/jeffposnick/status/1280223070876315649
|
||||
targets: 'chrome >= 56',
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
require.resolve('@babel/plugin-proposal-object-rest-spread'),
|
||||
require.resolve('@babel/plugin-proposal-optional-chaining'),
|
||||
require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function plugin(context, options) {
|
||||
const {outDir, baseUrl} = context;
|
||||
const {
|
||||
debug,
|
||||
offlineModeActivationStrategies,
|
||||
injectManifestConfig,
|
||||
reloadPopup,
|
||||
pwaHead,
|
||||
swCustom,
|
||||
swRegister,
|
||||
} = options;
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-pwa',
|
||||
|
||||
getThemePath() {
|
||||
return path.resolve(__dirname, './theme');
|
||||
},
|
||||
|
||||
getClientModules() {
|
||||
return isProd ? [swRegister] : [];
|
||||
},
|
||||
|
||||
configureWebpack(config) {
|
||||
if (!isProd) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin({
|
||||
PWA_DEBUG: debug,
|
||||
PWA_SERVICE_WORKER_URL: path.resolve(
|
||||
`${config.output.publicPath || '/'}`,
|
||||
'sw.js',
|
||||
),
|
||||
PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES: offlineModeActivationStrategies,
|
||||
PWA_RELOAD_POPUP: reloadPopup,
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
injectHtmlTags() {
|
||||
const headTags = [];
|
||||
if (isProd && pwaHead) {
|
||||
pwaHead.forEach(({tagName, ...attributes}) =>
|
||||
headTags.push({
|
||||
tagName,
|
||||
attributes,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return {headTags};
|
||||
},
|
||||
|
||||
async postBuild(props) {
|
||||
if (!isProd) {
|
||||
return;
|
||||
}
|
||||
|
||||
const swSourceFileTest = /\.m?js$/;
|
||||
|
||||
const swWebpackConfig = {
|
||||
entry: path.resolve(__dirname, 'sw.js'),
|
||||
output: {
|
||||
path: outDir,
|
||||
filename: 'sw.js',
|
||||
publicPath: baseUrl,
|
||||
},
|
||||
target: 'webworker',
|
||||
mode: debug ? 'development' : 'production',
|
||||
devtool: debug ? 'source-map' : false,
|
||||
optimization: {
|
||||
splitChunks: false,
|
||||
minimize: !debug,
|
||||
// see https://developers.google.com/web/tools/workbox/guides/using-bundlers#webpack
|
||||
minimizer: [
|
||||
!debug &&
|
||||
new Terser({
|
||||
test: swSourceFileTest,
|
||||
}),
|
||||
].filter(Boolean),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin({
|
||||
PWA_SW_CUSTOM: swCustom,
|
||||
}),
|
||||
new LogPlugin({
|
||||
name: 'Service Worker',
|
||||
color: 'red',
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: swSourceFileTest,
|
||||
exclude: /(node_modules)/,
|
||||
use: getSWBabelLoader(),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await compile([swWebpackConfig]);
|
||||
|
||||
const swDest = path.resolve(props.outDir, 'sw.js');
|
||||
|
||||
await injectManifest({
|
||||
...injectManifestConfig,
|
||||
globPatterns: [
|
||||
'**/*.{js,json,css,html}',
|
||||
'**/*.{png,jpg,jpeg,gif,svg,ico}',
|
||||
'**/*.{woff,woff2,eot,ttf,otf}',
|
||||
...(injectManifest.globPatterns || []),
|
||||
],
|
||||
// those attributes are not overrideable
|
||||
swDest,
|
||||
swSrc: swDest,
|
||||
globDirectory: props.outDir,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = plugin;
|
||||
|
||||
plugin.validateOptions = function validateOptions({validate, options}) {
|
||||
return validate(PluginOptionSchema, options);
|
||||
};
|
43
packages/docusaurus-plugin-pwa/src/pluginOptionSchema.js
Normal file
43
packages/docusaurus-plugin-pwa/src/pluginOptionSchema.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const Joi = require('@hapi/joi');
|
||||
const path = require('path');
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
debug: false,
|
||||
offlineModeActivationStrategies: ['appInstalled', 'queryString'],
|
||||
injectManifestConfig: {},
|
||||
pwaHead: [],
|
||||
swCustom: undefined,
|
||||
swRegister: path.join(__dirname, 'registerSw.js'),
|
||||
reloadPopup: '@theme/PwaReloadPopup',
|
||||
};
|
||||
|
||||
exports.PluginOptionSchema = Joi.object({
|
||||
debug: Joi.bool().default(DEFAULT_OPTIONS.debug),
|
||||
offlineModeActivationStrategies: Joi.array()
|
||||
.items(
|
||||
Joi.string()
|
||||
.valid('appInstalled', 'queryString', 'mobile', 'saveData', 'always')
|
||||
.required(),
|
||||
)
|
||||
.default(DEFAULT_OPTIONS.offlineModeActivationStrategies),
|
||||
injectManifestConfig: Joi.object().default(
|
||||
DEFAULT_OPTIONS.injectManifestConfig,
|
||||
),
|
||||
pwaHead: Joi.array()
|
||||
.items(Joi.object({tagName: Joi.string().required()}).unknown().required())
|
||||
.default(DEFAULT_OPTIONS.pwaHead),
|
||||
swCustom: Joi.string(),
|
||||
swRegister: Joi.alternatives()
|
||||
.try(Joi.string(), Joi.bool().valid(false))
|
||||
.default(DEFAULT_OPTIONS.swRegister),
|
||||
reloadPopup: Joi.alternatives()
|
||||
.try(Joi.string(), Joi.bool().valid(false))
|
||||
.default(DEFAULT_OPTIONS.reloadPopup),
|
||||
});
|
211
packages/docusaurus-plugin-pwa/src/registerSw.js
Normal file
211
packages/docusaurus-plugin-pwa/src/registerSw.js
Normal file
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// First: read the env variables (provided by Webpack)
|
||||
/* eslint-disable prefer-destructuring */
|
||||
const PWA_SERVICE_WORKER_URL = process.env.PWA_SERVICE_WORKER_URL;
|
||||
const PWA_RELOAD_POPUP = process.env.PWA_RELOAD_POPUP;
|
||||
const PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES =
|
||||
process.env.PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES;
|
||||
const PWA_DEBUG = process.env.PWA_DEBUG;
|
||||
/* eslint-enable prefer-destructuring */
|
||||
|
||||
const debug = PWA_DEBUG; // shortcut
|
||||
|
||||
async function clearRegistrations() {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
if (debug && registrations.length > 0) {
|
||||
console.log(
|
||||
`[Docusaurus-PWA][registerSW]: unregister service workers`,
|
||||
registrations,
|
||||
);
|
||||
}
|
||||
registrations.forEach((registration) => {
|
||||
registration.registration.unregister();
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
const MAX_MOBILE_WIDTH = 940;
|
||||
const APP_INSTALLED_KEY = 'docusaurus.pwa.appInstalled';
|
||||
|
||||
const OfflineModeActivationStrategiesImplementations = {
|
||||
always: () => true,
|
||||
mobile: () => window.innerWidth <= MAX_MOBILE_WIDTH,
|
||||
saveData: () => !!(navigator.connection && navigator.connection.saveData),
|
||||
appInstalled: () => !!localStorage.getItem(APP_INSTALLED_KEY),
|
||||
queryString: () =>
|
||||
new URLSearchParams(window.location.search).get('offlineMode') === 'true',
|
||||
};
|
||||
|
||||
function isOfflineModeEnabled() {
|
||||
const activeStrategies = PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.filter(
|
||||
(strategyName) => {
|
||||
const strategyImpl =
|
||||
OfflineModeActivationStrategiesImplementations[strategyName];
|
||||
return strategyImpl();
|
||||
},
|
||||
);
|
||||
const enabled = activeStrategies.length > 0;
|
||||
if (debug) {
|
||||
const logObject = {
|
||||
activeStrategies,
|
||||
availableStrategies: PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES,
|
||||
};
|
||||
if (enabled) {
|
||||
console.log(
|
||||
'[Docusaurus-PWA][registerSW]: offline mode enabled, because of activation strategies',
|
||||
logObject,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'[Docusaurus-PWA][registerSW]: offline mode disabled, because none of the offlineModeActivationStrategies could be used',
|
||||
logObject,
|
||||
);
|
||||
}
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
|
||||
function createServiceWorkerUrl(params) {
|
||||
const paramsQueryString = JSON.stringify(params);
|
||||
const url = `${PWA_SERVICE_WORKER_URL}?params=${encodeURIComponent(
|
||||
paramsQueryString,
|
||||
)}`;
|
||||
if (debug) {
|
||||
console.log(`[Docusaurus-PWA][registerSW]: service worker url`, {
|
||||
url,
|
||||
params,
|
||||
});
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log('[Docusaurus-PWA][registerSW]: debug mode enabled');
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
const {Workbox} = await import('workbox-window');
|
||||
|
||||
const offlineMode = isOfflineModeEnabled();
|
||||
|
||||
const url = createServiceWorkerUrl({offlineMode, debug});
|
||||
const wb = new Workbox(url);
|
||||
|
||||
const registration = await wb.register();
|
||||
|
||||
const sendSkipWaiting = () => wb.messageSW({type: 'SKIP_WAITING'});
|
||||
|
||||
const handleServiceWorkerWaiting = async () => {
|
||||
if (debug) {
|
||||
console.log('[Docusaurus-PWA][registerSW]: handleServiceWorkerWaiting');
|
||||
}
|
||||
// Immediately load new service worker when files aren't cached
|
||||
if (!offlineMode) {
|
||||
sendSkipWaiting();
|
||||
} else if (PWA_RELOAD_POPUP) {
|
||||
const renderReloadPopup = (await import('./renderReloadPopup')).default;
|
||||
renderReloadPopup({
|
||||
onReload() {
|
||||
wb.addEventListener('controlling', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
sendSkipWaiting();
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (debug) {
|
||||
if (registration.active) {
|
||||
console.log(
|
||||
'[Docusaurus-PWA][registerSW]: registration.active',
|
||||
registration,
|
||||
);
|
||||
}
|
||||
if (registration.installing) {
|
||||
console.log(
|
||||
'[Docusaurus-PWA][registerSW]: registration.installing',
|
||||
registration,
|
||||
);
|
||||
}
|
||||
if (registration.waiting) {
|
||||
console.log(
|
||||
'[Docusaurus-PWA][registerSW]: registration.waiting',
|
||||
registration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update service worker if the next one is already in the waiting state.
|
||||
// This happens when the user doesn't click on `reload` in the popup.
|
||||
if (registration.waiting) {
|
||||
handleServiceWorkerWaiting();
|
||||
}
|
||||
|
||||
// Update the current service worker when the next one has finished
|
||||
// installing and transitions to waiting state.
|
||||
wb.addEventListener('waiting', (event) => {
|
||||
if (debug) {
|
||||
console.log('[Docusaurus-PWA][registerSW]: event waiting', event);
|
||||
}
|
||||
handleServiceWorkerWaiting();
|
||||
});
|
||||
|
||||
// Update current service worker if the next one finishes installing and
|
||||
// moves to waiting state in another tab.
|
||||
wb.addEventListener('externalwaiting', (event) => {
|
||||
if (debug) {
|
||||
console.log(
|
||||
'[Docusaurus-PWA][registerSW]: event externalwaiting',
|
||||
event,
|
||||
);
|
||||
}
|
||||
handleServiceWorkerWaiting();
|
||||
});
|
||||
|
||||
window.addEventListener('appinstalled', (event) => {
|
||||
if (debug) {
|
||||
console.log('[Docusaurus-PWA][registerSW]: event appinstalled', event);
|
||||
}
|
||||
localStorage.setItem(APP_INSTALLED_KEY, true);
|
||||
|
||||
// After the app is installed, we register a service worker with the path
|
||||
// `/sw?enabled`. Since the previous service worker was `/sw`, it'll be
|
||||
// treated as a new one. The previous registration will need to be
|
||||
// cleared, otherwise the reload popup will show.
|
||||
clearRegistrations();
|
||||
});
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
if (debug) {
|
||||
console.log('[Docusaurus-PWA][registerSW]: event appinstalled', event);
|
||||
}
|
||||
// TODO instead of default browser install UI, show custom docusaurus prompt?
|
||||
// event.preventDefault();
|
||||
|
||||
if (localStorage.getItem(APP_INSTALLED_KEY)) {
|
||||
localStorage.removeItem(APP_INSTALLED_KEY);
|
||||
|
||||
// After uninstalling the app, if the user doesn't clear all data, then
|
||||
// the previous service worker will continue serving cached files. We
|
||||
// need to clear registrations and reload, otherwise the popup will show.
|
||||
clearRegistrations();
|
||||
}
|
||||
});
|
||||
} else if (debug) {
|
||||
console.log(
|
||||
'[Docusaurus-PWA][registerSW]: browser does not support service workers',
|
||||
);
|
||||
}
|
||||
})();
|
26
packages/docusaurus-plugin-pwa/src/renderReloadPopup.js
Normal file
26
packages/docusaurus-plugin-pwa/src/renderReloadPopup.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
|
||||
const POPUP_CONTAINER_ID = 'pwa-popup-container';
|
||||
|
||||
const getContainer = () => document.getElementById(POPUP_CONTAINER_ID);
|
||||
|
||||
const createContainer = () => {
|
||||
const container = document.createElement('div');
|
||||
container.id = POPUP_CONTAINER_ID;
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
};
|
||||
|
||||
export default async function renderReloadPopup(props) {
|
||||
const container = getContainer() || createContainer();
|
||||
const {default: ReloadPopup} = await import(process.env.PWA_RELOAD_POPUP);
|
||||
render(<ReloadPopup {...props} />, container);
|
||||
}
|
120
packages/docusaurus-plugin-pwa/src/sw.js
Normal file
120
packages/docusaurus-plugin-pwa/src/sw.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
import {PrecacheController} from 'workbox-precaching';
|
||||
|
||||
function parseSwParams() {
|
||||
const params = JSON.parse(
|
||||
new URLSearchParams(self.location.search).get('params'),
|
||||
);
|
||||
if (params.debug) {
|
||||
console.log('[Docusaurus-PWA][SW]: Service Worker params:', params);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
// doc advise against dynamic imports in SW
|
||||
// https://developers.google.com/web/tools/workbox/guides/using-bundlers#code_splitting_and_dynamic_imports
|
||||
// https://twitter.com/sebastienlorber/status/1280155204575518720
|
||||
// but I think it's working fine as it's inlined by webpack, need to double check?
|
||||
async function runSWCustomCode(params) {
|
||||
if (process.env.PWA_SW_CUSTOM) {
|
||||
const customSW = await import(process.env.PWA_SW_CUSTOM);
|
||||
if (typeof customSW.default === 'function') {
|
||||
customSW.default(params);
|
||||
} else if (params.debug) {
|
||||
console.warn(
|
||||
'[Docusaurus-PWA][SW]: swCustom should have a default export function',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets different possible variations for a request URL. Similar to
|
||||
* https://git.io/JvixK
|
||||
*
|
||||
* @param {String} url
|
||||
*/
|
||||
function getPossibleURLs(url) {
|
||||
const possibleURLs = [];
|
||||
const urlObject = new URL(url, self.location.href);
|
||||
|
||||
if (urlObject.origin !== self.location.origin) {
|
||||
return possibleURLs;
|
||||
}
|
||||
|
||||
// Ignore search params and hash
|
||||
urlObject.search = '';
|
||||
urlObject.hash = '';
|
||||
|
||||
// /blog.html
|
||||
possibleURLs.push(urlObject.href);
|
||||
|
||||
// /blog/ => /blog/index.html
|
||||
if (urlObject.pathname.endsWith('/')) {
|
||||
possibleURLs.push(`${urlObject.href}index.html`);
|
||||
} else {
|
||||
// /blog => /blog/index.html
|
||||
possibleURLs.push(`${urlObject.href}/index.html`);
|
||||
}
|
||||
|
||||
return possibleURLs;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const params = parseSwParams();
|
||||
|
||||
const precacheManifest = self.__WB_MANIFEST;
|
||||
const controller = new PrecacheController();
|
||||
|
||||
if (params.offlineMode) {
|
||||
controller.addToCacheList(precacheManifest);
|
||||
}
|
||||
|
||||
await runSWCustomCode(params);
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(controller.install());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(controller.activate());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', async (event) => {
|
||||
if (params.offlineMode) {
|
||||
const requestURL = event.request.url;
|
||||
const possibleURLs = getPossibleURLs(requestURL);
|
||||
for (let i = 0; i < possibleURLs.length; i += 1) {
|
||||
const possibleURL = possibleURLs[i];
|
||||
const cacheKey = controller.getCacheKeyForURL(possibleURL);
|
||||
if (cacheKey) {
|
||||
if (params.debug) {
|
||||
console.log('[Docusaurus-PWA][SW]: serving cached asset', {
|
||||
requestURL,
|
||||
possibleURL,
|
||||
possibleURLs,
|
||||
cacheKey,
|
||||
});
|
||||
}
|
||||
event.respondWith(caches.match(cacheKey));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('message', async (event) => {
|
||||
const type = event.data && event.data.type;
|
||||
|
||||
if (type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function PwaReloadPopup({onReload}) {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
return (
|
||||
isVisible && (
|
||||
<div className={clsx('alert', 'alert--secondary', styles.popup)}>
|
||||
<p>New version available</p>
|
||||
<div className={styles.buttonContainer}>
|
||||
<button
|
||||
className="button button--link"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsVisible(false);
|
||||
onReload();
|
||||
}}>
|
||||
Refresh
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="Close"
|
||||
className="close"
|
||||
type="button"
|
||||
onClick={() => setIsVisible(false)}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.popup {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
width: 344px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.popup p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.popup {
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.buttonContainer :global(.close) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue