VulpesOS/assets/webapps/system.gaiamobile.org/shared/js/icons_helper.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

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);