-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Experiment: Admin PWA #33102
Experiment: Admin PWA #33102
Changes from all commits
8807607
53e0e2e
0c9b67b
deb33fe
c6af99a
6b84258
f6f274e
8d6c0a7
fb80dc1
19cb71a
3a9e14e
a112140
5018965
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
/** | ||
* Progressive Web App | ||
* | ||
* @package gutenberg | ||
*/ | ||
|
||
add_filter( | ||
'admin_head', | ||
function() { | ||
$l10n = array( | ||
'logo' => file_get_contents( ABSPATH . 'wp-admin/images/wordpress-logo-white.svg' ), | ||
'siteTitle' => get_bloginfo( 'name' ), | ||
'adminUrl' => admin_url(), | ||
); | ||
wp_enqueue_script( 'wp-admin-manifest' ); | ||
wp_localize_script( 'wp-admin-manifest', 'wpAdminManifestL10n', $l10n ); | ||
} | ||
); | ||
|
||
add_filter( | ||
'load-index.php', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PWA plugin makes use of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, we could do that too :) |
||
function() { | ||
if ( ! isset( $_GET['service-worker'] ) ) { | ||
return; | ||
} | ||
|
||
header( 'Content-Type: text/javascript' ); | ||
// Must be at the admin root so the scope is correct. Move to the | ||
// wp-admin folder when merging with core. | ||
echo file_get_contents( __DIR__ . '/service-worker.js' ); | ||
exit; | ||
} | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* global self */ | ||
|
||
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting | ||
self.addEventListener( 'install', function ( event ) { | ||
event.waitUntil( self.skipWaiting() ); | ||
} ); | ||
|
||
// https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim | ||
self.addEventListener( 'activate', function ( event ) { | ||
event.waitUntil( self.clients.claim() ); | ||
} ); | ||
|
||
// Necessary for Chrome to show the install button. | ||
self.addEventListener( 'fetch', function () {} ); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package-lock=false |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Admin Manifest | ||
|
||
Dynamically creates a Web App [manifest](https://w3c.github.io/manifest/) and registers the service worker for the admin. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "@wordpress/admin-manifest", | ||
"version": "1.0.0", | ||
"description": "Dynamically creates a Web App manifest and registers the service worker for the admin.", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you mark it as private so it doesn't get published to npm until it's ready for prime time? It will also remove the entry from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, sure, I wasn't aware of that. |
||
"author": "The WordPress Contributors", | ||
"license": "GPL-2.0-or-later", | ||
"keywords": [ | ||
"wordpress", | ||
"gutenberg", | ||
"manifest" | ||
], | ||
"homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/admin-manifest/README.md", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/WordPress/gutenberg.git", | ||
"directory": "packages/admin-manifest" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/WordPress/gutenberg/issues" | ||
}, | ||
"engines": { | ||
"node": ">=12" | ||
}, | ||
"main": "build/index.js", | ||
"module": "build-module/index.js", | ||
"react-native": "src/index", | ||
"dependencies": { | ||
"@babel/runtime": "^7.13.10" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
function addManifest( manifest ) { | ||
const link = document.createElement( 'link' ); | ||
link.rel = 'manifest'; | ||
link.href = 'data:application/manifest+json,' + JSON.stringify( manifest ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is clever. The PWA plugin uses the REST API to serve the manifest, but using a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #33102 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is clever. The PWA plugin uses the REST API to serve the manifest, but using a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, this simplifies things and allows us to dynamically generate the icon (for which I'm changing the background color based on the admin color scheme). See #33102 (comment). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess it's still preferable to use the REST API for the frontend web app manifest, because there's a performance hit for running script with each page load. |
||
document.head.appendChild( link ); | ||
} | ||
|
||
function addAppleTouchIcon( size, base64data ) { | ||
const iconLink = document.createElement( 'link' ); | ||
iconLink.rel = 'apple-touch-icon'; | ||
iconLink.href = base64data; | ||
iconLink.sizes = '180x180'; | ||
document.head.insertBefore( iconLink, document.head.firstElementChild ); | ||
} | ||
|
||
function createSvgElement( html ) { | ||
const doc = document.implementation.createHTMLDocument( '' ); | ||
doc.body.innerHTML = html; | ||
const { firstElementChild: svgElement } = doc.body; | ||
svgElement.setAttribute( 'viewBox', '0 0 80 80' ); | ||
return svgElement; | ||
} | ||
|
||
function createIcon( { svgElement, size, color, backgroundColor, circle } ) { | ||
return new Promise( ( resolve ) => { | ||
const canvas = document.createElement( 'canvas' ); | ||
const context = canvas.getContext( '2d' ); | ||
|
||
// Leave 1/8th padding around the logo. | ||
const padding = size / 8; | ||
// Which leaves 3/4ths of space for the icon. | ||
const logoSize = padding * 6; | ||
|
||
// Resize the SVG logo. | ||
svgElement.setAttribute( 'width', logoSize ); | ||
svgElement.setAttribute( 'height', logoSize ); | ||
|
||
// Color in the background. | ||
svgElement.querySelectorAll( 'path' ).forEach( ( path ) => { | ||
path.setAttribute( 'fill', backgroundColor ); | ||
} ); | ||
|
||
// Resize the canvas. | ||
canvas.width = size; | ||
canvas.height = size; | ||
|
||
// If we're not drawing a circle, set the background color. | ||
if ( ! circle ) { | ||
context.fillStyle = backgroundColor; | ||
context.fillRect( 0, 0, canvas.width, canvas.height ); | ||
} | ||
|
||
// Fill in the letter (W) and circle around it. | ||
context.fillStyle = color; | ||
context.beginPath(); | ||
context.arc( size / 2, size / 2, logoSize / 2 - 1, 0, 2 * Math.PI ); | ||
context.closePath(); | ||
context.fill(); | ||
|
||
// Create a URL for the SVG to load in an image element. | ||
const svgBlob = new window.Blob( [ svgElement.outerHTML ], { | ||
type: 'image/svg+xml', | ||
} ); | ||
const url = URL.createObjectURL( svgBlob ); | ||
const image = document.createElement( 'img' ); | ||
|
||
image.src = url; | ||
image.width = logoSize; | ||
image.height = logoSize; | ||
image.onload = () => { | ||
// Once the image is loaded, draw it onto the canvas. | ||
context.drawImage( image, padding, padding ); | ||
// Export it to a blob. | ||
canvas.toBlob( ( imageBlob ) => { | ||
// We no longer need the SVG blob url. | ||
URL.revokeObjectURL( url ); | ||
// Unfortunately blob URLs don't seem to work, so we have to use | ||
// base64 encoded data URLs. | ||
const reader = new window.FileReader(); | ||
reader.readAsDataURL( imageBlob ); | ||
reader.onloadend = () => { | ||
resolve( reader.result ); | ||
}; | ||
} ); | ||
}; | ||
} ); | ||
} | ||
|
||
// eslint-disable-next-line @wordpress/no-global-event-listener | ||
window.addEventListener( 'load', () => { | ||
if ( ! ( 'serviceWorker' in window.navigator ) ) { | ||
return; | ||
} | ||
|
||
const { logo, siteTitle, adminUrl } = window.wpAdminManifestL10n; | ||
const manifest = { | ||
// Replace spaces with non breaking spaces. Chrome collapses them. | ||
name: siteTitle.replace( / /g, ' ' ), | ||
display: 'standalone', | ||
orientation: 'portrait', | ||
start_url: adminUrl, | ||
// Open front-end, login page, and any external URLs in a browser | ||
// modal. | ||
scope: adminUrl, | ||
icons: [], | ||
}; | ||
|
||
const adminBar = document.getElementById( 'wpadminbar' ); | ||
const { color, backgroundColor } = window.getComputedStyle( adminBar ); | ||
const svgElement = createSvgElement( logo ); | ||
|
||
Promise.all( [ | ||
// The maskable icon should have its background filled. This is used | ||
// for iOS. To do: check which sizes are really needed. | ||
...[ 180, 192, 512 ].map( ( size ) => | ||
createIcon( { | ||
svgElement, | ||
size, | ||
color, | ||
backgroundColor, | ||
} ).then( ( base64data ) => { | ||
manifest.icons.push( { | ||
src: base64data, | ||
sizes: size + 'x' + size, | ||
type: 'image/png', | ||
purpose: 'maskable', | ||
} ); | ||
|
||
// iOS doesn't seem to look at the manifest. | ||
if ( size === 180 ) { | ||
addAppleTouchIcon( size, base64data ); | ||
} | ||
} ) | ||
), | ||
// The "normal" icon should be round. This is used for Chrome | ||
// Desktop PWAs. To do: check which sizes are really needed. | ||
...[ 180, 192, 512 ].map( ( size ) => | ||
createIcon( { | ||
svgElement, | ||
size, | ||
color, | ||
backgroundColor, | ||
circle: true, | ||
} ).then( ( base64data ) => { | ||
manifest.icons.push( { | ||
src: base64data, | ||
sizes: size + 'x' + size, | ||
type: 'image/png', | ||
purpose: 'any', | ||
} ); | ||
} ) | ||
), | ||
] ).then( () => { | ||
addManifest( manifest ); | ||
window.navigator.serviceWorker.register( adminUrl + '?service-worker' ); | ||
} ); | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this incorporate the site icon? Otherwise a user may have multiple admin PWAs and they'd all have the same icon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've thought a bit about this, and I think we shouldn't use the site icon. See #33102 (comment).
I was using the site icon at first, but it only works with pngs in Chrome, and the front-end of the site could potentially be a PWA that uses the site icon. The site icon is meant to be used for the site, not the administration area. I've made it so that the admin icon is dynamically generated based on the admin color scheme, so you could have many different WordPress sites that you're managing with differently coloured icons. I think this is the nicest solution for the admin, for which we'd need a default icon anyway if so site icon is set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How often do users change the admin color scheme? I personally never change it. It's the same for all sites I manage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I'm not sure about that. Is not the site icon also showing up as the favicon when in the admin?