let warnedBefore = false; const warn = () => { if (warnedBefore) { return; } // We will only warn the user if the Badging API is not available at all if ('ExperimentalBadge' in window || 'setExperimentalAppBadge' in navigator) { return; } console.warn('Badging API must be enabled. Please check here how you can enable it: https://developers.google.com/web/updates/2018/12/badging-api#use'); warnedBefore = true; }; const current = { mediaQuery: null, value: 0, }; function isVersion1Available() { return 'ExperimentalBadge' in window && !!window.ExperimentalBadge; } function isVersion2Available() { return ('setExperimentalAppBadge' in navigator && !!navigator.setExperimentalAppBadge && 'clearExperimentalAppBadge' in navigator && !!navigator.clearExperimentalAppBadge); } function isAvailable() { if (!current.mediaQuery) { current.mediaQuery = window.matchMedia('(display-mode: standalone)'); // Get notified once app is installed current.mediaQuery.onchange = event => { set(current.value); }; } return (current.mediaQuery.matches && (isVersion1Available() || isVersion2Available())); } function set(value) { current.value = value; if (!isAvailable()) { warn(); return false; } // Sets the badge to contents (an integer), or to "flag" if contents is omitted. If contents is 0, clears the badge for the matching app(s). // See details here: https://github.com/WICG/badging/blob/master/explainer.md#the-api if (isVersion1Available()) { window.ExperimentalBadge.set(value); return true; } else if (isVersion2Available()) { navigator.setExperimentalAppBadge(value); return true; } return false; } function clear() { if (!isAvailable()) { return; } if (isVersion1Available()) { window.ExperimentalBadge.clear(); } else if (isVersion2Available()) { navigator.clearExperimentalAppBadge(); } } function deepMerge(target, source) { // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties for (const key of Object.keys(source)) { if (source[key] instanceof Object) Object.assign(source[key], deepMerge(target[key], source[key])); } // Join `target` and modified `source` Object.assign(target || {}, source); return target; } function isPositiveNumber(value) { return (typeof value !== 'undefined' && Number.isInteger(value) && value >= 0); } const DefaultValue = 0; const DefaultOptions = { backgroundColor: '#424242', color: '#ffffff', indicator: '!', radius: 3, size: 7, horizontalMargin: 0, verticalMargin: 0, horizontalPadding: 1, verticalPadding: 1, }; const isFirefox = navigator.userAgent.indexOf('Firefox') > -1; // Get all favicons of the page const getFavicons = () => { const links = document.head.getElementsByTagName('link'); const favicons = []; for (let i = 0; i < links.length; i++) { const link = links[i]; const href = link.getAttribute('href'); const rel = link.getAttribute('rel'); if (!href) { continue; } if (!rel) { continue; } if (rel.split(' ').indexOf('icon') === -1) { continue; } favicons.push(link); } return favicons; }; // Get the favicon with the best quality of the document const getBestFavicon = () => { const favicons = getFavicons(); let bestFavicon = null; let bestSize = 0; for (let i = 0; i < favicons.length; i++) { const favicon = favicons[i]; const href = favicon.getAttribute('href'); const sizes = favicon.getAttribute('sizes'); // If the href looks like it's an SVG, it's the best we can get if (href === null || href === void 0 ? void 0 : href.endsWith('.svg')) { return favicon; } // If the link does not have a "sizes" attribute, we use it only if we haven't found anything else yet if (!sizes) { if (!bestFavicon) { bestFavicon = favicon; bestSize = 0; } continue; } // If we find an icon with sizes "any", it's the best we can get if (sizes === 'any') { return favicon; } // Otherwise we will try to find the maximum size const size = parseInt(sizes.split('x')[0], 10); if (Number.isNaN(size)) { if (!bestFavicon) { bestFavicon = favicon; bestSize = 0; } continue; } if (size > bestSize) { bestFavicon = favicon; bestSize = size; continue; } } return bestFavicon; }; // References to the favicons that we need to track in order to reset and update the counters const current$1 = { favicons: null, bestFavicon: null, bestFaviconImage: null, value: DefaultValue, options: DefaultOptions, }; // Get size depending on screen density const devicePixelRatioListener = window.matchMedia('screen and (min-resolution: 2dppx)'); const getRatio = () => { return Math.ceil(window.devicePixelRatio) || 1; }; const handleRatioChange = () => { set$1(current$1.value, current$1.options); }; const getIconSize = () => { return 16 * getRatio(); }; // Update favicon const setFavicon = (url) => { if (!url) { return; } // Remove previous favicons for (const favicon of getFavicons()) { if (favicon.parentNode) { favicon.parentNode.removeChild(favicon); } } // Create new favicon const newFavicon = document.createElement('link'); newFavicon.id = 'badgin'; newFavicon.type = 'image/x-icon'; newFavicon.rel = 'icon favicon'; newFavicon.href = url; document.getElementsByTagName('head')[0].appendChild(newFavicon); }; // Draw the favicon const drawFavicon = (image, value, options) => { const iconSize = getIconSize(); const canvas = document.createElement('canvas'); canvas.width = iconSize; canvas.height = iconSize; const context = canvas.getContext('2d'); if (!context) { return; } // Draw new image image.width = iconSize; image.height = iconSize; context.drawImage(image, 0, 0, image.width, image.height); // Draw bubble on the top drawBubble(context, value, options); // Refresh tag in page setFavicon(canvas.toDataURL()); }; // Draws the bubble on the canvas const drawBubble = (context, value, options) => { const ratio = getRatio(); const iconSize = getIconSize(); // Do we need to render the bubble at all? let finalValue = ''; if (isPositiveNumber(value)) { if (value === 0) { finalValue = ''; } else if (value < 100) { finalValue = String(value); } else { finalValue = '99+'; } } else { finalValue = options.indicator; } // Return early if (!finalValue) { return; } // Calculate text width initially const textHeight = options.size - 2; const font = `${options.size * ratio}px Arial`; context.font = font; const { width: textWidth } = context.measureText(finalValue); context.restore(); // Calculate position etc. const width = textWidth + 2 * options.horizontalPadding; const height = textHeight * ratio + 2 * options.verticalPadding; const top = iconSize - height - options.verticalMargin; const left = iconSize - width - options.horizontalMargin; const bottom = 16 * ratio - options.verticalMargin; const right = 16 * ratio - options.horizontalMargin; const radius = options.radius; // Bubble context.globalAlpha = 1; context.fillStyle = options.backgroundColor; context.strokeStyle = options.backgroundColor; context.lineWidth = 0; context.beginPath(); context.moveTo(left + radius, top); context.quadraticCurveTo(left, top, left, top + radius); context.lineTo(left, bottom - radius); context.quadraticCurveTo(left, bottom, left + radius, bottom); context.lineTo(right - radius, bottom); context.quadraticCurveTo(right, bottom, right, bottom - radius); context.lineTo(right, top + radius); context.quadraticCurveTo(right, top, right - radius, top); context.closePath(); context.fill(); context.save(); // Value context.font = font; context.fillStyle = options.color; context.textAlign = 'center'; context.textBaseline = 'hanging'; context.fillText(finalValue, left + width / 2, top + options.verticalPadding + (isFirefox ? 1 : 0)); context.save(); /* // Helper line context.restore() context.strokeStyle = '#ff0000' context.moveTo(0, top + height / 2) context.lineTo(iconSize, top + height / 2) context.stroke() context.save() */ }; function isAvailable$1() { return !!getBestFavicon(); } function set$1(value, options) { // Remember options current$1.value = value; deepMerge(current$1.options, options || {}); if (!isAvailable$1()) { return false; } // Remember favicons if (!current$1.bestFavicon) { const bestFavicon = getBestFavicon(); if (bestFavicon) { const bestFaviconImage = document.createElement('img'); // Allow cross origin resource requests if the image is not a data:uri if (!bestFavicon.href.match(/^data/)) { bestFaviconImage.crossOrigin = 'anonymous'; } // Load image bestFaviconImage.src = bestFavicon.href; // Store for next time current$1.bestFavicon = bestFavicon; current$1.bestFaviconImage = bestFaviconImage; } // Once the device pixel ratio changes we set the value again devicePixelRatioListener.addEventListener('change', handleRatioChange); } if (!current$1.favicons) { current$1.favicons = getFavicons(); } // The image is required for setting the badge if (!current$1.bestFaviconImage) { return false; } // If we have the image, we can draw immediately if (current$1.bestFaviconImage.complete) { drawFavicon(current$1.bestFaviconImage, current$1.value, current$1.options); return true; } // Otherwise we will wait for the load event current$1.bestFaviconImage.addEventListener('load', function () { drawFavicon(this, current$1.value, current$1.options); }); return true; } function clear$1() { if (!isAvailable$1()) { return; } // Reset value and options current$1.value = DefaultValue; current$1.options = DefaultOptions; // Remove old listener devicePixelRatioListener.removeEventListener('change', handleRatioChange); if (current$1.favicons) { // Remove current favicons for (const favicon of getFavicons()) { if (favicon.parentNode) { favicon.parentNode.removeChild(favicon); } } // Recreate old favicons for (const favicon of current$1.favicons) { document.head.appendChild(favicon); } current$1.favicons = null; current$1.bestFavicon = null; current$1.bestFaviconImage = null; } } const defaultOptions = { indicator: '!', }; const current$2 = { title: null, value: 0, options: defaultOptions, }; function changeTitle(title, value, options) { let newTitle = title; if (isPositiveNumber(value)) { if (value === 0) { newTitle = title; } else { newTitle = `(${value}) ${title}`; } } else { newTitle = `(${options.indicator}) ${title}`; } const element = document.querySelector('title'); if (element) { element.childNodes[0].nodeValue = newTitle; } } function set$2(value, options) { if (current$2.title === null) { current$2.title = document.title; // Watch changes of title Object.defineProperty(document, 'title', { get: () => { return current$2.title; }, set: title => { current$2.title = title; changeTitle(current$2.title, current$2.value, current$2.options); }, }); } // Remember value and options current$2.value = value; deepMerge(current$2.options, options || {}); // Trigger change document.title = document.title; return true; } function clear$2() { current$2.value = 0; // Trigger change document.title = document.title; } /** * Sets badge */ function set$3(value, options = {}) { switch (options.method) { case undefined: case 'Badging': { if (set(value)) { // Break only if method is explicitly requested if (options.method === 'Badging') { break; } } } case 'Favicon': { if (set$1(value, options.favicon)) { break; } } default: { set$2(value, options.title); } } } /** * Clears badge */ function clear$3() { clear(); clear$1(); clear$2(); } export { clear$3 as clear, set$3 as set };