Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
441 lines
13 KiB
JavaScript
441 lines
13 KiB
JavaScript
/* globals Icon */
|
|
/* exported IconsHelper */
|
|
'use strict';
|
|
|
|
/**
|
|
* Utility library that will help us to work with icons coming from
|
|
* different sources.
|
|
*/
|
|
(function IconsHelper(exports) {
|
|
const ICON_CACHE_PERIOD = 24 * 60 * 60 * 1000; // 1 day
|
|
const FETCH_XHR_TIMEOUT = 10000;
|
|
const DEBUG = false;
|
|
|
|
var dataStore = null;
|
|
|
|
/**
|
|
* Return default size in px based in devicePixelRatio
|
|
*
|
|
* Sized based on current homescreen selected icons for apps
|
|
* in a configuration of 3 icons per row. See:
|
|
* https://github.com/mozilla-b2g/gaia/blob/master/
|
|
* shared/elements/gaia_grid/js/grid_layout.js#L15
|
|
* @returns {Number}
|
|
*/
|
|
function getDefaultIconSize() {
|
|
var dpr = window.devicePixelRatio;
|
|
return (dpr && dpr > 1) ? 142 : 84;
|
|
}
|
|
|
|
function sizeIsNearer(size1, size2, targetSize) {
|
|
// TODO: weight for a larger vs. smaller icon?
|
|
var delta1 = Math.abs(targetSize - size1);
|
|
var delta2 = Math.abs(targetSize - size2);
|
|
return (delta1 <= delta2);
|
|
}
|
|
|
|
/**
|
|
* Return a promise that resolves to the URL of the best icon for a web page
|
|
* given its meta data and web manifest.
|
|
*
|
|
* @param uri {string}
|
|
* @param iconTargetSize {number}
|
|
* @param placeObj {Object}
|
|
* @param siteObj {Object}
|
|
* @returns {Promise}
|
|
*/
|
|
function getIcon(uri, iconTargetSize, placeObj = {}, siteObj = {}) {
|
|
var iconUrl = null;
|
|
|
|
iconTargetSize = iconTargetSize * window.devicePixelRatio;
|
|
|
|
// First look for an icon in the Webmanifest.
|
|
if (siteObj.webManifestUrl && siteObj.webManifest) {
|
|
iconUrl = getBestIconFromWebManifest(siteObj.webManifest, iconTargetSize);
|
|
if (DEBUG && iconUrl) {
|
|
console.log('Icon from Web Manifest');
|
|
}
|
|
}
|
|
|
|
// Then look for an icon in the Firefox manifest.
|
|
if (!iconUrl && siteObj.manifest && siteObj.manifest.icons) {
|
|
iconUrl = getBestIconFromWebManifest({
|
|
icons: _convertToWebManifestIcons(siteObj.manifest,
|
|
siteObj.origin || siteObj.manifest.origin)
|
|
}, iconTargetSize);
|
|
if (DEBUG && iconUrl) {
|
|
console.log('Icon from Firefox App Manifest');
|
|
}
|
|
}
|
|
|
|
// Otherwise, look into the meta tags.
|
|
if (!iconUrl && placeObj && placeObj.icons) {
|
|
iconUrl = getBestIconFromMetaTags(placeObj.icons, iconTargetSize);
|
|
if (DEBUG && iconUrl) {
|
|
console.log('Icon from Meta tags');
|
|
}
|
|
}
|
|
|
|
// Last resort, we look for a favicon.ico file.
|
|
if (!iconUrl) {
|
|
var a = document.createElement('a');
|
|
a.href = uri;
|
|
iconUrl = a.origin + '/favicon.ico';
|
|
if (iconTargetSize) {
|
|
iconUrl += '#-moz-resolution=' + iconTargetSize + ',' + iconTargetSize;
|
|
}
|
|
DEBUG && console.log('Icon from favicon.ico');
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
resolve(iconUrl);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Same as above except the promise resolves as an object containing the blob
|
|
* of the icon and its size in pixels.
|
|
*
|
|
* @param uri {string}
|
|
* @param iconTargetSize {number}
|
|
* @param placeObj {Object}
|
|
* @param siteObj {Object}
|
|
* @returns {Promise}
|
|
*/
|
|
function getIconBlob(uri, iconTargetSize, placeObj = {}, siteObj = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
getIcon(uri, iconTargetSize, placeObj, siteObj)
|
|
.then(iconUrl => {
|
|
// @todo Need a better syntax.
|
|
getStore().then(iconStore => {
|
|
iconStore.get(iconUrl).then(iconObj => {
|
|
if (!iconObj || !iconObj.timestamp ||
|
|
Date.now() - iconObj.timestamp >= ICON_CACHE_PERIOD) {
|
|
return fetchIconBlob(iconUrl)
|
|
.then(iconBlob => {
|
|
var img = document.createElement('img');
|
|
var icon = new Icon(img, uri);
|
|
icon.renderBlob(iconBlob, {
|
|
size: iconTargetSize,
|
|
onLoad: function(blob) {
|
|
var iconObj = {
|
|
blob: blob,
|
|
originalUrl: iconUrl.toString(),
|
|
timestamp: Date.now()
|
|
};
|
|
// We resolve here to avoid I/O blocking on
|
|
// dataStore and quicker display.
|
|
// Persisting to the dataStore takes place subsequently.
|
|
resolve(iconObj);
|
|
|
|
iconStore.add(iconObj, iconUrl);
|
|
},
|
|
onerror: function(e) {
|
|
reject(`Failed to fetch icon ${iconUrl}`);
|
|
}
|
|
});
|
|
})
|
|
.catch(err => {
|
|
reject(`Failed to fetch icon ${iconUrl}: ${err}`);
|
|
});
|
|
}
|
|
|
|
return resolve(iconObj);
|
|
}).catch(err => {
|
|
// We should fetch the icon and resolve the promise here, anyhow.
|
|
reject(`Failed to get icon from dataStore: ${err}`);
|
|
});
|
|
}).catch(err => {
|
|
// We should fetch the icon and resolve the promise here, anyhow.
|
|
reject(`Error opening the dataStore: ${err}`);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Same as above but set the image as the icon property of a gaia-app-icon
|
|
* element.
|
|
*
|
|
* @param icon {Object?}
|
|
* @param targetSize {number}
|
|
* @returns {Promise}
|
|
*/
|
|
function setElementIcon(icon, targetSize) {
|
|
return getIconBlob(icon.bookmark.url, targetSize,
|
|
icon.bookmark, icon.bookmark)
|
|
.then(iconObj => {
|
|
if (iconObj.blob) {
|
|
icon.icon = iconObj.blob;
|
|
return Promise.resolve();
|
|
} else if (icon.bookmark.icon) {
|
|
// We fallback to the bookmark.icon property if no icons were found.
|
|
return fetchIconBlob(icon.bookmark.icon)
|
|
.then(iconBlob => {
|
|
icon.icon = iconBlob;
|
|
return Promise.resolve();
|
|
}, Promise.reject.bind(Promise)); // XXXbz Why second arg?
|
|
}
|
|
return Promise.reject('No icon data found');
|
|
}, Promise.reject.bind(Promise)); // XXXbz Why bother with second arg?
|
|
}
|
|
|
|
function getBestIconFromWebManifest(webManifest, iconSize) {
|
|
var icons = webManifest.icons;
|
|
if (!icons) {
|
|
return null;
|
|
}
|
|
var maxSize = 10000;
|
|
|
|
var bestSize = maxSize;
|
|
var iconURL = null;
|
|
iconSize = iconSize || getDefaultIconSize();
|
|
|
|
icons.forEach((potentialIcon) => {
|
|
if (!iconURL) {
|
|
iconURL = potentialIcon.src;
|
|
}
|
|
|
|
var sizes = Array.from(potentialIcon.sizes);
|
|
var nearestSize = getNearestSize(sizes, iconSize, bestSize);
|
|
|
|
if (nearestSize !== bestSize &&
|
|
sizeIsNearer(nearestSize, bestSize, iconSize)) {
|
|
iconURL = potentialIcon.src;
|
|
bestSize = nearestSize;
|
|
}
|
|
});
|
|
return iconURL ? iconURL : null;
|
|
}
|
|
|
|
function _convertToWebManifestIcons(manifest, origin) {
|
|
return Object.keys(manifest.icons).map(function(size) {
|
|
var url = manifest.icons[size];
|
|
var sizes = [size + 'x' + size];
|
|
url = url.indexOf('http') > -1 ? url : origin + url;
|
|
|
|
return {
|
|
src: new URL(url),
|
|
sizes: sizes
|
|
};
|
|
});
|
|
}
|
|
|
|
// See bug 1041482, we will need to support better
|
|
// icons for different part of the system application.
|
|
// A web page have different ways to defining icons
|
|
// based on size, 'touch' capabilities and so on.
|
|
// From gecko we will receive all the rel='icon'
|
|
// defined which will contain as well the sizes
|
|
// supported in that file.
|
|
// This function will help to deliver the best suitable
|
|
// icon based on that definition list.
|
|
// The expected format is the following one:
|
|
//
|
|
// {
|
|
// '[uri 1]': {
|
|
// sizes: ['16x16 32x32 48x48', '60x60']
|
|
// },
|
|
// '[uri 2]': {
|
|
// sizes: ['16x16']
|
|
// }
|
|
// }
|
|
//
|
|
// iconSize is an additional parameter to specify a concrete
|
|
// size or the closest icon.
|
|
function getBestIconFromMetaTags(icons, iconSize) {
|
|
if (!icons) {
|
|
return null;
|
|
}
|
|
iconSize = iconSize || getDefaultIconSize();
|
|
var iconURL = null;
|
|
var bestSize = 10000;
|
|
|
|
Object.keys(icons).forEach((uri) => {
|
|
var potentialIcon = icons[uri];
|
|
if (!iconURL) {
|
|
// Handle the case of no size info in the whole list
|
|
iconURL = uri;
|
|
}
|
|
var sizes = Array.from(potentialIcon.sizes);
|
|
var nearestSize = getNearestSize(sizes, iconSize, bestSize);
|
|
|
|
if (nearestSize !== bestSize &&
|
|
sizeIsNearer(nearestSize, bestSize, iconSize)) {
|
|
iconURL = uri;
|
|
bestSize = nearestSize;
|
|
}
|
|
if (potentialIcon.rel === 'apple-touch-icon' ||
|
|
potentialIcon.rel === 'apple-touch-icon-precomposed') {
|
|
var moreInfoUrl = 'https://developer.mozilla.org/en-US/' +
|
|
'Apps/Build/Icon_implementation_for_apps#General_icons_for_web_apps';
|
|
console.warn('Warning: The apple-touch icons are being used ' +
|
|
'as a fallback only. They will be deprecated in ' +
|
|
'the future. See ' + moreInfoUrl);
|
|
}
|
|
});
|
|
return iconURL || null;
|
|
}
|
|
|
|
// Given an array of size strings e.g. (64x64), a target iconSize
|
|
// and optionally a best-so-far-size,
|
|
// return the nearest match
|
|
function getNearestSize(sizes, iconSize, bestSize) {
|
|
var bogusSize = 10000;
|
|
if (!bestSize) {
|
|
bestSize = bogusSize;
|
|
}
|
|
var nearestSize = sizes.reduce(function(nearestSize, sizeString, idx) {
|
|
var size = widthFromSizeString(sizeString);
|
|
if (isNaN(size)) {
|
|
return nearestSize;
|
|
}
|
|
if (sizeIsNearer(size, nearestSize, iconSize)) {
|
|
return size;
|
|
} else {
|
|
return nearestSize;
|
|
}
|
|
}, bestSize);
|
|
return nearestSize === bogusSize ? -1 : nearestSize;
|
|
}
|
|
|
|
// Given an icon size by string YYxYY returns the
|
|
// width measurement, so will assume this will be
|
|
// used by strings that identify a square size.
|
|
function widthFromSizeString(size) {
|
|
size = size || '';
|
|
var xIndex = size.indexOf('x');
|
|
if (!xIndex) {
|
|
return NaN;
|
|
}
|
|
return parseInt(size.substr(0, xIndex));
|
|
}
|
|
|
|
/**
|
|
* Return a promise that resolves to a dataStore for icons.
|
|
*
|
|
* @returns {Promise}
|
|
*/
|
|
function getStore() {
|
|
return new Promise(resolve => {
|
|
if (dataStore) {
|
|
return resolve(dataStore);
|
|
}
|
|
navigator.getDataStores('icons').then(stores => {
|
|
dataStore = stores[0];
|
|
return resolve(dataStore);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear all the icons in the store.
|
|
*
|
|
* @returns {Promise}
|
|
*/
|
|
function clear() {
|
|
return getStore().then(iconStore => {
|
|
iconStore.clear();
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Return a promise that resolves to an object containing the blob url
|
|
* in pixels of an icon given its URL `iconUrl`.
|
|
*
|
|
* @param {string} iconUrl
|
|
* @returns {Promise}
|
|
*/
|
|
function fetchIcon(iconUrl) {
|
|
return new Promise((resolve, reject) => {
|
|
fetchIconBlob(iconUrl)
|
|
.then((iconBlob) => {
|
|
var img = document.createElement('img');
|
|
|
|
img.src = URL.createObjectURL(iconBlob);
|
|
|
|
img.onload = () => {
|
|
var iconSize = Math.max(img.naturalWidth, img.naturalHeight);
|
|
|
|
resolve({
|
|
blob: iconBlob,
|
|
url: iconUrl,
|
|
size: iconSize,
|
|
timestamp: Date.now()
|
|
});
|
|
};
|
|
|
|
img.onerror = () => {
|
|
reject(new Error(`Error while loading image.`));
|
|
};
|
|
})
|
|
.catch((e) => {
|
|
reject(new Error(`Error while loading image: ${e}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Return a promise that resolves to an object containing the blob
|
|
* given its URL `iconUrl`.
|
|
*
|
|
* @param {string} iconUrl
|
|
* @returns {Promise}
|
|
*/
|
|
function fetchIconBlob(iconUrl) {
|
|
return new Promise((resolve, reject) => {
|
|
var xhr = new XMLHttpRequest({
|
|
mozAnon: true,
|
|
mozSystem: true
|
|
});
|
|
|
|
xhr.open('GET', iconUrl, true);
|
|
xhr.responseType = 'blob';
|
|
xhr.timeout = FETCH_XHR_TIMEOUT;
|
|
|
|
// Remember that send() can throw for some non http protocols.
|
|
// The promise wrapper here protects us.
|
|
xhr.send();
|
|
|
|
xhr.onload = () => {
|
|
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
|
|
var iconBlob = xhr.response;
|
|
resolve(iconBlob);
|
|
|
|
return;
|
|
}
|
|
|
|
reject(new Error(
|
|
`Got HTTP status ${xhr.status} trying to load ${iconUrl}.`));
|
|
};
|
|
|
|
xhr.onerror = xhr.ontimeout = () => {
|
|
reject(new Error(`Error while getting ${iconUrl}.`));
|
|
};
|
|
});
|
|
}
|
|
|
|
exports.IconsHelper = {
|
|
getIcon: getIcon,
|
|
getIconBlob: getIconBlob,
|
|
|
|
setElementIcon: setElementIcon,
|
|
|
|
getBestIconFromWebManifest: getBestIconFromWebManifest,
|
|
getBestIconFromMetaTags: getBestIconFromMetaTags,
|
|
|
|
fetchIcon: fetchIcon,
|
|
fetchIconBlob: fetchIconBlob,
|
|
|
|
get defaultIconSize() {
|
|
return getDefaultIconSize();
|
|
},
|
|
|
|
clear: clear,
|
|
|
|
// Make public for unit test purposes.
|
|
getNearestSize: getNearestSize,
|
|
};
|
|
|
|
})(window);
|