diff --git a/.gitignore b/.gitignore index e43b0f9..9daa824 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +node_modules diff --git a/README.md b/README.md index 3789662..066a286 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,56 @@ -pwacompat is a drop-in JS companion library to help your modern website be more progressive. -This takes a `manifest.json` file and, where possible, provides support to non-standard browsers such as Safari on iOS. +pwacompat is a library to help your modern website be more progressive. +This takes a [Web App Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest), and, where possible, provides support to non-standard browsers such as Safari on iOS. # Usage +## Drop-In + Drop-in the `pwacompat.min.js` script into your page directly to get its benefits. Add this script tag anywhere after your manifest file is included, e.g., at the bottom of your page- ```html + ``` **Warning!** Don't use the `pwacompat.js` file directly, as it's written in ES6, which can't be run natively in most browsers. +### Browsers + +This is supported in most modern browsers (UC Browser, Safari, Firefox, Chrome, IE9+). + +## Library + +You can also use pwacompat as a library, where it will convert a manifest to HTML that can be inserted into your document's header. +Install with NPM- + +```bash +$ npm install --save pwacompat +``` + +And then include as part of your build process to generate templates- + +```js +const pwacompat = require('pwacompat'); +const html = compat(require('./path/to/manifest.json')); +console.info(html); // prints '\n' and so on +``` + # Details pwacompat performs a few main tasks- * Creates fallback meta tags for older devices (e.g., iOS, older WebKit/Chromium forks etc) * Creates meta icon tags for all icons in the manifest -* For webapps added to an [iOS homescreen](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html#//apple_ref/doc/uid/TP40002051-CH3-SW2)- - * Ensures your webapp always starts at your `start_url` - * Fixes internal navigation so users stay within the webapp -# Requirements +The drop-in version also provides JS that enhances webapps added to an [iOS homescreen](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html#//apple_ref/doc/uid/TP40002051-CH3-SW2)- -If your site has a [Web App Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest), you're good to go. -Your `
` tag should contain- - -```html - -``` +* Ensures your webapp always starts at your `start_url` +* Fixes internal navigation so users stay within the webapp -And the `manifest.json` file itself should look a bit like- +# Web App Manifest + +Your Web App Manifest is normally named `manifest.json`, live at `/manifest.json`, and should look a bit like this- ```js { @@ -49,20 +68,3 @@ And the `manifest.json` file itself should look a bit like- For more information on Web App Manifest, and how e.g., modern browsers will prompt engaged users to install your site to their home screen, check out [Google Developers](https://developers.google.com/web/updates/2014/11/Support-for-installable-web-apps-with-webapp-manifest-in-chrome-38-for-Android). To learn about how to be more progressive with your features, supporting all your users, check out [Always Be Progressive](https://samthor.github.io/AlwaysBeProgressive/). - -## Browsers - -This is supported in most modern browsers (UC Browser, Safari, Firefox, Chrome, IE9+). - -# Release - -Compile code with [Closure Compiler](https://closure-compiler.appspot.com/home). - -``` -// ==ClosureCompiler== -// @compilation_level ADVANCED_OPTIMIZATIONS -// @output_file_name pwacompat.min.js -// ==/ClosureCompiler== - -// code here -``` \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..b4d982e --- /dev/null +++ b/index.js @@ -0,0 +1,32 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +'use strict'; + +const parse = require('./lib'); +const escapeHTML = require('escape-html'); + +module.exports = function(manifest) { + const tags = parse(manifest); + const text = tags.map(tag => { + const attrs = Object.keys(tag.attr).map(name => { + const v = tag.attr[name]; + return `${name}="${escapeHTML(v)}"` + }); + return `<${tag.name} ${attrs.join(' ')}/>` + }); + return text.join('\n'); +}; \ No newline at end of file diff --git a/lib.js b/lib.js new file mode 100644 index 0000000..8a39475 --- /dev/null +++ b/lib.js @@ -0,0 +1,95 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +'use strict'; + +/** + * @param {!Object} manifest Parsed JSON manifest. + * @return {!Array<{name: string, attr: !Object}>} + */ +function parse(manifest) { + const out = []; + + function createMeta(name, value) { + if (!value) { return; } + out.push({ + name: 'meta', + attr: { + 'name': name, + 'content': value === true ? 'yes' : value, + }, + }); + } + + const capable = ['standalone', 'fullscreen'].indexOf(manifest['display']) !== -1; + createMeta('apple-mobile-web-app-capable', capable); + createMeta('mobile-web-app-capable', capable); + createMeta('apple-mobile-web-app-title', manifest['short_name'] || manifest['name']); + createMeta('msapplication-starturl', manifest['start_url'] || '/'); + createMeta('msapplication-TileColor', manifest['theme_color']); + + // nb. pwacompat does _not_ create the meta 'theme-color', as browsers that support the manifest + // file don't use its 'theme_color' when the webpage is just loaded in a normal browser (as of + // July 2016). So be sure to set it yourself. + + // TODO(samthor): Set 'apple-mobile-web-app-status-bar-style' to 'black' for dark theme-color, + // and use 'default' for light theme-color. + + let itunes; + (manifest['related_applications'] || []) + .filter(app => app['platform'] == 'itunes') + .forEach(app => { + if (app['id']) { + itunes = app['id']; + } else { + const match = app['url'].match(/id(\d+)/); + if (match) { + itunes = match[1]; + } + } + }); + if (itunes) { + createMeta('apple-itunes-app', `app-id=${itunes}`) + } + + // Parse the icons. + const icons = manifest['icons'] || []; + icons.sort((a, b) => { + return parseInt(b.sizes, 10) - parseInt(a.sizes, 10); // sort larger first + }); + icons.forEach(icon => { + out.push({ + name: 'icon', + attr: { + 'rel': 'icon', + 'href': icon.src, + 'sizes': icon.sizes, + }, + }); + out.push({ + name: 'icon', + attr: { + 'rel': 'apple-touch-icon', + 'href': icon.src, + 'sizes': icon.sizes, + }, + }); + }); + + return out; +} + +module.exports = parse; diff --git a/package.json b/package.json index 8f8f765..d96903d 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "pwacompat", - "version": "1.0.0", + "version": "1.0.1", "description": "library to bring Web App Manifest files to older browsers", - "main": "pwacompat.min.js", + "main": "index.js", "repository": { "type": "git", "url": "git+https://github.com/GoogleChrome/pwacompat.git" @@ -19,5 +19,8 @@ "bugs": { "url": "https://github.com/GoogleChrome/pwacompat/issues" }, - "homepage": "https://github.com/GoogleChrome/pwacompat" + "homepage": "https://github.com/GoogleChrome/pwacompat", + "dependencies": { + "escape-html": "^1.0.3" + } } diff --git a/pwacompat.js b/pwacompat.js index 81d8250..1e2fce4 100644 --- a/pwacompat.js +++ b/pwacompat.js @@ -57,69 +57,13 @@ } function processManifest(manifest) { - /** - * @param {string} name - * @param {string|boolean|null} value - */ - function createMeta(name, value) { - if (!value) { return; } - if (document.querySelector(`meta[name="${name}"]`)) { return; } - const tag = document.createElement('meta'); - tag.setAttribute('name', name); - tag.setAttribute('content', value === true ? 'yes' : value); - document.head.appendChild(tag); - } - - const capable = ['standalone', 'fullscreen'].indexOf(manifest['display']) !== -1; - createMeta('apple-mobile-web-app-capable', capable); - createMeta('mobile-web-app-capable', capable); - createMeta('apple-mobile-web-app-title', manifest['short_name'] || manifest['name']); - createMeta('msapplication-starturl', manifest['start_url'] || '/'); - createMeta('msapplication-TileColor', manifest['theme_color']); - - /* - * nb. pwacompat does _not_ create the meta 'theme-color', as browsers that support the manifest - * file don't use its 'theme_color' when the webpage is just loaded in a normal browser (as of - * July 2016). So be sure to set it yourself. - */ - - let itunes; - (manifest['related_applications'] || []) - .filter(app => app['platform'] == 'itunes') - .forEach(app => { - if (app['id']) { - itunes = app['id']; - } else { - const match = app['url'].match(/id(\d+)/); - if (match) { - itunes = match[1]; - } - } - }); - if (itunes) { - createMeta('apple-itunes-app', `app-id=${itunes}`) - } - - // nb. this doesn't set 'apple-mobile-web-app-status-bar-style', as using 'black-translucent' - // moves the page up behind the status bar. - // TODO(samthor): Use white for a bright theme-color, black for a dark one. - - // Parse the icons. - const icons = manifest['icons'] || []; - icons.sort((a, b) => { - // sort larger first - return parseInt(b.sizes, 10) - parseInt(a.sizes, 10); - }); - icons.forEach(icon => { - const iconEl = document.createElement('link'); - iconEl.setAttribute('rel', 'icon'); - iconEl.setAttribute('href', icon.src); - iconEl.setAttribute('sizes', icon.sizes); - document.head.appendChild(iconEl); - - const appleIconEl = iconEl.cloneNode(true); - appleIconEl.setAttribute('rel', 'apple-touch-icon'); - document.head.appendChild(appleIconEl); + const parse = require('./lib'); + parse(manifest).forEach(tag => { + const node = document.createElement(tag.name); + for (const k in tag.attr) { + node.setAttribute(k, tag.attr[k]); + } + document.head.appendChild(node); }); // If this is a standalone iOS ATHS app, perform setup actions. diff --git a/pwacompat.min.js b/pwacompat.min.js index ea40c67..5cd7054 100644 --- a/pwacompat.min.js +++ b/pwacompat.min.js @@ -1,5 +1,6 @@ -(function(){function f(a){if(navigator.standalone){var c;try{c=JSON.parse(window.localStorage["pwacompat.js"])}catch(d){}if(c){a(c);return}}var b=new XMLHttpRequest;b.onload=function(){var c=JSON.parse(b.responseText);try{window.localStorage["pwacompat.js"]=b.responseText}catch(k){}a(c)};b.open("GET",e.href);b.send()}function g(a){function c(a,b){if(b&&!document.querySelector('meta[name="'+a+'"]')){var c=document.createElement("meta");c.setAttribute("name",a);c.setAttribute("content",!0===b?"yes": -b);document.head.appendChild(c)}}var b=-1!==["standalone","fullscreen"].indexOf(a.display);c("apple-mobile-web-app-capable",b);c("mobile-web-app-capable",b);c("apple-mobile-web-app-title",a.short_name||a.name);c("msapplication-starturl",a.start_url||"/");c("msapplication-TileColor",a.theme_color);var d;(a.related_applications||[]).filter(function(a){return"itunes"==a.platform}).forEach(function(a){a.id?d=a.id:(a=a.url.match(/id(\d+)/))&&(d=a[1])});d&&c("apple-itunes-app","app-id="+d);b=a.icons||[]; -b.sort(function(a,b){return parseInt(b.sizes,10)-parseInt(a.sizes,10)});b.forEach(function(a){var b=document.createElement("link");b.setAttribute("rel","icon");b.setAttribute("href",a.src);b.setAttribute("sizes",a.sizes);document.head.appendChild(b);a=b.cloneNode(!0);a.setAttribute("rel","apple-touch-icon");document.head.appendChild(a)});navigator.standalone&&h(a)}function h(a){document.addEventListener("click",function(a){"A"===a.target.tagName&&((new URL(a.target.href)).origin!==location.origin? -window.localStorage["pwacompat.js:out"]=location.href:(a.preventDefault(),window.location=a.target.href))});if(window.sessionStorage&&!window.sessionStorage.loaded){window.sessionStorage.loaded=!0;a=window.localStorage["pwacompat.js:out"]||a.start_url;delete window.localStorage["pwacompat.js:out"];var c=window.location.href+window.location.search;a&&a!=c&&(a.replace(/#.*$/,"")==c?window.location.hash=a.substr(a.indexOf("#")):window.location=a)}}if(!navigator.serviceWorker&&window.localStorage){var e= -document.head.querySelector('link[rel="manifest"]');e&&e.href?f(g):console.warn('pwacompat.js can\'t operate: no found')}})(); +(function l(h,d,e){function f(a,g){if(!d[a]){if(!h[a]){var b="function"==typeof require&&require;if(!g&&b)return b(a,!0);if(c)return c(a,!0);g=Error("Cannot find module '"+a+"'");throw g.code="MODULE_NOT_FOUND",g;}g=d[a]={b:{}};h[a][0].call(g.b,function(b){var g=h[a][1][b];return f(g?g:b)},g,g.b,l,h,d,e)}return d[a].b}for(var c="function"==typeof require&&require,b=0;b