Никита Дубко, HR Tech Яндекса
Никита Дубко, HR Tech Яндекса
Web Standards Days в Минске, 2016
Progressive Web Apps:
Escaping Tabs Without Losing Our Soul
caniuse
Project Fugu. Is it edible? – Nikita Dubko
Никита Дубко — WebHID API: управление почти чем угодно из браузера
Mozilla, like Google, is looking ahead to the end of Apple's WebKit rule
Checking out and building Chromium for iOS
Project Fugu API Showcase
Squoosh
Excalidraw
SVGCode
// app.manifest
"related_applications": [
{
"platform": "webapp",
"url": "https://my.app/app.webmanifest"
}
],
if ('getInstalledRelatedApps' in navigator) {
const relatedApps =
await navigator.getInstalledRelatedApps();
const PWAisInstalled = relatedApps.length > 0;
if (PWAisInstalled) {
installButton.classList.add('hidden');
}
}
PWA Detection
@media (display-mode: standalone),
(display-mode: window-controls-overlay) {
.pwa-install-button {
display: none;
}
}
// app.manifest
"short_name": "D&D Tokenizer",
"description": "Some description.",
"screenshots": [
{
"src": "./screens/desktop.jpg",
"type": "image/jpeg",
"sizes": "800x583",
"form_factor": "wide"
},
{
"src": "./screens/mobile.jpg",
"type": "image/jpeg",
"sizes": "530x700",
"form_factor": "narrow"
}
],
// app.manifest
"share_target": {
"action": "/?share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"files": [
{
"name": "image",
"accept": ["image/*"]
}
]
}
},
// sw.js
self.addEventListener('fetch', (fetchEvent) => {
const url = new URL(fetchEvent.request.url);
if (
url.pathname === '/' &&
url.searchParams.has('share-target') &&
fetchEvent.request.method === 'POST'
) {
return fetchEvent.respondWith(
(async () => {
const formData = await fetchEvent.request.formData();
const image = formData.get('image');
const keys = await caches.keys();
const sharedCache = await caches.open(
keys.filter((key) => key.startsWith('share-target'))[0]
);
await sharedCache.put('shared-image', new Response(image));
return Response.redirect('./?share-target', 303);
})()
);
}
});
Receiving shared data with the Web Share Target API
// main.js
window.addEventListener('load', async () => {
if (location.search.includes('share-target')) {
const keys = await caches.keys();
const sharedCache = await caches.open(
keys.filter((key) => key.startsWith('share-target'))[0]
);
const image = await sharedCache.match('shared-image');
if (image) {
const blob = await image.blob();
await sharedCache.delete('shared-image');
// do something with blob
}
}
});
Receiving shared data with the Web Share Target API
const shareButton = document.querySelector('.button');
if (navigator.canShare) {
shareButton.addEventListener('click', async () => {
const blob = getBlobFromImage();
const file = new File([blob], 'token.png', {
type: 'image/png',
});
const data = {
files: [file],
};
if (navigator.canShare(data)) {
try {
await navigator.share(data);
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err.name, err.message);
}
}
}
});
}
Integrate with the OS sharing UI with the Web Share API
document.addEventListener('paste', async (e) => {
e.preventDefault();
const clipboardItems = typeof navigator?.clipboard?.read === 'function'
? await navigator.clipboard.read()
: e.clipboardData.files;
for (const clipboardItem of clipboardItems) {
let blob;
if (clipboardItem.type?.startsWith('image/')) {
blob = clipboardItem;
// do something with blob
} else {
const imageTypes = clipboardItem.types?.filter((type) =>
type.startsWith('image/')
);
for (const imageType of imageTypes) {
blob = await clipboardItem.getType(imageType);
// do something with blob
}
}
}
});
Unblocking clipboard access
// app.manifest
"display": "standalone",
"display_override": ["window-controls-overlay"],
/* style.css */
@media (display-mode: window-controls-overlay) {
.header {
padding-right: calc(2 * env(titlebar-area-x));
padding-left: env(titlebar-area-x);
height: calc(env(titlebar-area-height) + 10px);
}
}
.header__title {
-webkit-app-region: drag;
}
Window Controls Overlay for Installed Desktop Web Apps
const supportsFileSystemAccess =
'showOpenFilePicker' in window &&
(() => {
try {
return window.self === window.top;
} catch {
return false;
}
})();
if (supportsFileSystemAccess) {
let fileHandle = undefined;
try {
[fileHandle] = await showOpenFilePicker();
return await fileHandle.getFile();
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err.name, err.message);
}
}
}
Reading and writing files and directories with the browser-fs-access library
let promise = navigator.runOnOsLogin.set({
mode: "windowed",
});
promise.then(function() {
// Пользователь дал добро
},
function(reason) {
// Что-то пошло не так
});
Run on OS Login
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
// Пользователь выбрал пиксель с цветом:
const colorHexValue = result.sRGBHex;
} catch (err) {
// Пользователь выключил пипетку
}
// app.manifest
"protocol_handlers": [
{
"protocol": "web+tokenizer",
"url": "/?from=%s"
}
],
URL protocol handler registration for PWAs
// app.manifest
"capture_links": "existing_client_event",
"url_handlers": [
{
"origin": "https://dnd-tokenizer-41471e.netlify.app"
}
]
Test
PWAs as URL Handlers
// app.manifest
"shortcuts": [
{
"name": "Create new token",
"url": "/"
}
],
"launch_handler": {
"client_mode": "focus-existing"
},
Shortcuts Explainer
// app.manifest
"file_handlers": [
{
"action": "/",
"accept": {
"image/*": [
".jpg", ".jpeg", ".webp",
".png", ".avif", ".gif"
]
}
}
],
Handle files in Progressive Web Apps
// main.js
if ('launchQueue' in window) {
launchQueue.setConsumer((launchParams) => {
const files = launchParams.files;
for (const file of files) {
const blob = await file.getFile();
blob.handle = file;
// Сделать что-то с файлом
}
});
} else {
console.error('File Handling API is not supported!');
}
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot
.getFileHandle('file', { create: true });
const directoryHandle = await opfsRoot
.getDirectoryHandle('folder', { create: true });
const nestedFileHandle = await directoryHandle
.getFileHandle('nested file', { create: true });
const nestedDirectoryHandle = await directoryHandle
.getDirectoryHandle('nested folder', { create: true });
The origin private file system
try {
const availableFonts = await window.queryLocalFonts();
const list = document.querySelector('select.fonts');
for (const fontData of availableFonts) {
const option = document.createElement('option');
option.text = fontData.fullName;
option.value = fontData.postscriptName;
if (fontData.fullName === 'Times New Roman') {
option.selected = true;
}
list?.appendChild(option);
}
} catch (err) {
console.error(err.name, err.message);
}
Use advanced typography with local fonts