You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1001 lines
28 KiB

'use strict'
/* eslint-env browser */
/* globals chrome, Utils */
const { agent, open, i18n, getOption, setOption, promisify, sendMessage } =
Utils
const baseUrl = 'https://www.wappalyzer.com'
const utm = '?utm_source=popup&utm_medium=extension&utm_campaign=wappalyzer'
const footers = [
{
heading: 'Generate sales leads',
body: 'Find new prospects by the technologies they use. Reach out to customers of Shopify, Magento, Salesforce and others.',
buttonText: 'Create a lead list',
buttonLink: `${baseUrl}/lists/${utm}`,
},
{
heading: 'Connect Wappalyzer to your CRM',
body: 'See the technology stacks of your leads without leaving your CRM. Connect to HubSpot, Pipedrive and many others.',
buttonText: 'See all apps',
buttonLink: `${baseUrl}/apps/${utm}`,
},
{
heading: 'Enrich your data with tech stacks',
body: 'Upload a list of websites to get a report of the technologies in use, such as CMS or ecommerce platforms.',
buttonText: 'Upload a list',
buttonLink: `${baseUrl}/lookup/${utm}#bulk`,
},
{
heading: 'Automate technology lookups',
body: 'Our APIs provide instant access to website technology stacks, contact details and social media profiles.',
buttonText: 'Compare APIs',
buttonLink: `${baseUrl}/api/${utm}`,
},
{
heading: 'Wappalyzer for businesses',
body: 'Sign up for a plan to get monthly credits to spend on any product, including lead lists and technology lookups.',
buttonText: 'Compare plans',
buttonLink: `${baseUrl}/pricing/${utm}`,
},
]
const attributeKeys = [
'phone',
'skype',
'whatsapp',
'email',
'verifiedEmail',
'safeEmail',
'twitter',
'facebook',
'instagram',
'github',
'tiktok',
'youtube',
'pinterest',
'linkedin',
'owler',
'title',
'description',
'copyright',
'copyrightYear',
'responsive',
'schemaOrgTypes',
'certInfo.subjectOrg',
'certInfo.subjectCountry',
'certInfo.subjectState',
'certInfo.subjectLocality',
'certInfo.issuer',
'certInfo.protocol',
'certInfo.validTo',
'dns.spf',
'dns.dmarc',
'https',
'trackerGoogleAnalytics',
'trackerGoogleAdSense',
'trackerMedianet',
'trackerFacebook',
'trackerOptimizely',
'companyName',
'inferredCompanyName',
'industry',
'about',
'locations',
'companySize',
'companyType',
'companyFounded',
'employees',
]
function setDisabledDomain(enabled) {
const el = {
headerSwitchEnabled: document.querySelector('.header__switch--enabled'),
headerSwitchDisabled: document.querySelector('.header__switch--disabled'),
}
if (enabled) {
el.headerSwitchEnabled.classList.add('header__switch--hidden')
el.headerSwitchDisabled.classList.remove('header__switch--hidden')
} else {
el.headerSwitchEnabled.classList.remove('header__switch--hidden')
el.headerSwitchDisabled.classList.add('header__switch--hidden')
}
}
function getCsv() {
let hostname = ''
let www = false
let https = false
try {
let protocol = ''
;({ hostname, protocol } = new URL(Popup.cache.url))
www = hostname.startsWith('www')
https = protocol === 'https:'
hostname = hostname.replace(/^www\./, '')
} catch (error) {
// Continue
}
const columns = [
'URL',
...Popup.cache.categories.map(({ id }) =>
chrome.i18n.getMessage(`categoryName${id}`)
),
...attributeKeys.map((key) =>
chrome.i18n.getMessage(
`attribute${
key.charAt(0).toUpperCase() + key.slice(1).replace('.', '_')
}`
)
),
]
const csv = [`"${columns.join('","')}"`]
const filename = `wappalyzer${
hostname ? `_${hostname.replace('.', '-')}` : ''
}.csv`
const row = [`http${https ? 's' : ''}://${www ? 'www.' : ''}${hostname}`]
row.push(
...Popup.cache.categories.reduce((categories, { id }) => {
categories.push(
Popup.cache.detections
.filter(({ categories }) =>
categories.some(({ id: _id }) => _id === id)
)
.map(({ name }) => name)
.join(' ; ')
)
return categories
}, [])
)
row.push(
...attributeKeys.map((key) => csvEscape(Popup.cache.attributeValues[key]))
)
csv.push(`"${row.join('","')}"`)
return { csv, filename }
}
function csvEscape(value = '') {
if (Array.isArray(value)) {
value = value
.flat()
.slice(0, 10)
.map((value) => csvEscape(String(value).replace(/ ; /g, ' : ')))
.join(' ; ')
}
if (typeof value === 'string') {
return value.replace(/\n/g, ' ').replace(/"/g, '""').trim()
}
if (typeof value === 'boolean') {
return String(value).toUpperCase()
}
if (value === null) {
return ''
}
return String(value).replace(/"/g, '""')
}
function parseEmail(fullEmail) {
const email = fullEmail.replace(/^[^<]*<([^>]+)>/, '$1')
const [name, title] = fullEmail.replace(/ <([^>]+)>$/, '').split(' -- ')
return { email, name, title }
}
function getTechnologySpend(technologies) {
const totals = technologies.reduce(
(totals, { pricing }) => {
pricing.forEach((price) => totals[price]++)
return totals
},
{ low: 0, poa: 0, mid: 0, high: 0 }
)
totals.mid += Math.floor(totals.low / 3)
totals.high += Math.floor(totals.poa / 2)
totals.high += Math.floor(totals.mid / 3)
totals.xhigh = Math.floor(totals.high / 3)
const spend = totals.xhigh
? 'Very high'
: totals.high
? 'High'
: totals.mid
? 'Medium'
: totals.low
? 'Low'
: 'Very low'
return spend
}
const Popup = {
/**
* Initialise popup
*/
async init() {
Popup.cache = {
url: '',
categories: [],
detections: [],
attributeValues: {},
}
const el = {
body: document.body,
terms: document.querySelector('.terms'),
detections: document.querySelector('.detections'),
playGame: document.querySelector('.empty__play-game'),
game: document.querySelector('.ttt-game'),
footer: document.querySelector('.footer'),
tabPlus: document.querySelector('.tab--plus'),
termsButtonAccept: document.querySelector('.terms__button--accept'),
termsButtonDecline: document.querySelector('.terms__button--decline'),
headerSwitches: document.querySelectorAll('.header__switch'),
headerSwitchEnabled: document.querySelector('.header__switch--enabled'),
headerSwitchDisabled: document.querySelector('.header__switch--disabled'),
plusConfigureApiKey: document.querySelector('.plus-configure__apikey'),
plusConfigureSave: document.querySelector('.plus-configure__save'),
plusDownload: document.querySelector('.plus-download'),
plusDownloadLink: document.querySelector(
'.plus-download__button .button__link'
),
headerSettings: document.querySelector('.header__settings'),
headerThemes: document.querySelectorAll('.header__theme'),
headerThemeLight: document.querySelector('.header__theme--light'),
headerThemeDark: document.querySelector('.header__theme--dark'),
templates: document.querySelectorAll('[data-template]'),
tabs: document.querySelectorAll('.tab'),
tabItems: document.querySelectorAll('.tab-item'),
credits: document.querySelector('.credits'),
issue: document.querySelector('.issue'),
footerHeadingText: document.querySelector('.footer__heading-text'),
footerContentBody: document.querySelector('.footer__content-body'),
footerButtonText: document.querySelector('.footer .button__text'),
footerButtonLink: document.querySelector('.footer .button__link'),
footerToggleClose: document.querySelector('.footer__toggle--close'),
footerToggleOpen: document.querySelector('.footer__toggle--open'),
footerHeading: document.querySelector('.footer__heading'),
}
// Templates
Popup.templates = Array.from(el.templates).reduce((templates, template) => {
templates[template.dataset.template] = template.cloneNode(true)
template.remove()
return templates
}, {})
// Disabled domains
const dynamicIcon = await getOption('dynamicIcon', false)
if (dynamicIcon) {
el.body.classList.add('dynamic-icon')
}
// Disabled domains
let disabledDomains = await getOption('disabledDomains', [])
// Dark mode
const theme = await getOption('theme', 'light')
if (theme === 'dark') {
el.body.classList.add('dark')
el.headerThemeLight.classList.remove('header__icon--hidden')
el.headerThemeDark.classList.add('header__icon--hidden')
}
// Terms
const termsAccepted =
agent === 'chrome' || (await getOption('termsAccepted', false))
if (termsAccepted) {
el.terms.classList.add('terms--hidden')
Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
} else {
el.terms.classList.remove('terms--hidden')
el.empty.classList.add('empty--hidden')
el.detections.classList.add('detections--hidden')
el.issue.classList.add('issue--hidden')
el.footer.classList.add('footer--hidden')
el.tabPlus.classList.add('tab--disabled')
el.termsButtonAccept.addEventListener('click', async () => {
await setOption('termsAccepted', true)
await setOption('tracking', true)
el.terms.classList.add('terms--hidden')
el.footer.classList.remove('footer--hidden')
el.tabPlus.classList.remove('tab--disabled')
Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
})
el.termsButtonDecline.addEventListener('click', async () => {
await setOption('termsAccepted', true)
await setOption('tracking', false)
el.terms.classList.add('terms--hidden')
el.footer.classList.remove('footer--hidden')
el.tabPlus.classList.remove('tab--disabled')
Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
})
}
let url
const tabs = await promisify(chrome.tabs, 'query', {
active: true,
currentWindow: true,
})
if (tabs && tabs.length) {
;[{ url }] = tabs
if (url.startsWith('http')) {
Popup.cache.url = url
const { hostname } = new URL(url)
setDisabledDomain(disabledDomains.includes(hostname))
el.headerSwitchDisabled.addEventListener('click', async () => {
disabledDomains = disabledDomains.filter(
(_hostname) => _hostname !== hostname
)
await setOption('disabledDomains', disabledDomains)
setDisabledDomain(false)
Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
})
el.headerSwitchEnabled.addEventListener('click', async () => {
disabledDomains.push(hostname)
await setOption('disabledDomains', disabledDomains)
setDisabledDomain(true)
Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
})
} else {
for (const headerSwitch of el.headerSwitches) {
headerSwitch.classList.add('header__switch--hidden')
}
el.tabPlus.classList.add('tab--disabled')
}
}
// Plus configuration
el.plusConfigureApiKey.value = await getOption('apiKey', '')
el.plusConfigureSave.addEventListener('click', async (event) => {
await setOption('apiKey', el.plusConfigureApiKey.value)
await Popup.getPlus(url)
})
// Header
el.headerSettings.addEventListener('click', () =>
chrome.runtime.openOptionsPage()
)
// Theme
el.headerThemes.forEach((headerTheme) =>
headerTheme.addEventListener('click', async () => {
const theme = await getOption('theme', 'light')
el.body.classList[theme === 'dark' ? 'remove' : 'add']('dark')
el.body.classList[theme === 'dark' ? 'add' : 'remove']('light')
el.headerThemeDark.classList[theme === 'dark' ? 'remove' : 'add'](
'header__icon--hidden'
)
el.headerThemeLight.classList[theme === 'dark' ? 'add' : 'remove'](
'header__icon--hidden'
)
await setOption('theme', theme === 'dark' ? 'light' : 'dark')
})
)
// Tabs
el.tabs.forEach((tab, index) => {
tab.addEventListener('click', async () => {
el.tabs.forEach((tab) => tab.classList.remove('tab--active'))
el.tabItems.forEach((item) => item.classList.add('tab-item--hidden'))
tab.classList.add('tab--active')
el.tabItems[index].classList.remove('tab-item--hidden')
el.credits.classList.add('credits--hidden')
el.plusDownload.classList.remove('plus-download--hidden')
el.footer.classList.remove('footer--hidden')
if (tab.classList.contains('tab--plus')) {
await Popup.getPlus(url)
}
})
})
// Download
el.plusDownloadLink.addEventListener('click', Popup.downloadCsv)
// Footer
const item =
footers[
Math.round(Math.random())
? 0
: Math.round(Math.random() * (footers.length - 1))
]
el.footerHeadingText.textContent = item.heading
el.footerContentBody.textContent = item.body
el.footerButtonText.textContent = item.buttonText
el.footerButtonLink.href = item.buttonLink
const collapseFooter = await getOption('collapseFooter', false)
if (collapseFooter) {
el.footer.classList.add('footer--collapsed')
el.footerToggleClose.classList.add('footer__toggle--hidden')
el.footerToggleOpen.classList.remove('footer__toggle--hidden')
}
el.footerHeading.addEventListener('click', async () => {
const collapsed = el.footer.classList.contains('footer--collapsed')
el.footer.classList[collapsed ? 'remove' : 'add']('footer--collapsed')
el.footerToggleClose.classList[collapsed ? 'remove' : 'add'](
'footer__toggle--hidden'
)
el.footerToggleOpen.classList[collapsed ? 'add' : 'remove'](
'footer__toggle--hidden'
)
await setOption('collapseFooter', !collapsed)
})
Array.from(document.querySelectorAll('a[href^="http"]')).forEach((a) => {
a.addEventListener('click', (event) => {
event.preventDefault()
event.stopImmediatePropagation()
const { version } = chrome.runtime.getManifest()
open(a.href.replace(/__URL__/g, url).replace(/__VERSION__/g, version))
return false
})
})
// Game
el.playGame.addEventListener('click', (event) => {
event.preventDefault()
event.stopImmediatePropagation()
el.playGame.classList.add('empty__play-game--hidden')
el.game.classList.remove('ttt-game--hidden')
})
// Apply internationalization
i18n()
Popup.cache.categories = await Popup.driver('getCategories')
},
driver(func, args) {
return sendMessage('popup.js', func, args)
},
/**
* Log debug messages to the console
* @param {String} message
*/
log(message) {
Popup.driver('log', message)
},
/**
* Group technologies into categories
* @param {Object} technologies
*/
categorise(technologies) {
return Object.values(
technologies
.filter(({ confidence }) => confidence >= 50)
.reduce((categories, technology) => {
technology.categories.forEach((category) => {
categories[category.id] = categories[category.id] || {
...category,
technologies: [],
}
categories[category.id].technologies.push(technology)
})
return categories
}, {})
)
},
/**
* Callback for getDetection listener
* @param {Array} detections
*/
async onGetDetections(detections = []) {
Popup.cache.detections = detections
const el = {
empty: document.querySelector('.empty'),
playGame: document.querySelector('.empty__play-game'),
game: document.querySelector('.ttt-game'),
detections: document.querySelector('.detections'),
issue: document.querySelector('.issue'),
plusDownload: document.querySelector('.plus-download'),
}
detections = (detections || [])
.filter(({ confidence }) => confidence >= 50)
.filter(({ slug }) => slug !== 'cart-functionality')
if (!detections || !detections.length) {
el.empty.classList.remove('empty--hidden')
el.playGame.classList.remove('empty__play-game--hidden')
el.game.classList.add('ttt-game--hidden')
el.detections.classList.add('detections--hidden')
el.issue.classList.add('issue--hidden')
el.plusDownload.classList.add('plus-download--hidden')
return
}
el.empty.classList.add('empty--hidden')
el.detections.classList.remove('detections--hidden')
el.issue.classList.remove('issue--hidden')
el.plusDownload.classList.remove('plus-download--hidden')
while (el.detections.firstChild) {
el.detections.removeChild(detections.firstChild)
}
const pinnedCategory = await getOption('pinnedCategory')
const categorised = Popup.categorise(detections)
categorised.forEach(({ id, name, slug: categorySlug, technologies }) => {
const categoryNode = Popup.templates.category.cloneNode(true)
const el = {
detections: document.querySelector('.detections'),
link: categoryNode.querySelector('.category__link'),
pins: categoryNode.querySelectorAll('.category__pin'),
pinsActive: document.querySelectorAll('.category__pin--active'),
}
el.link.href = `https://www.wappalyzer.com/technologies/${categorySlug}/?utm_source=popup&utm_medium=extension&utm_campaign=wappalyzer`
el.link.dataset.i18n = `categoryName${id}`
if (pinnedCategory === id) {
el.pins.forEach((pin) => pin.classList.add('category__pin--active'))
}
el.pins.forEach((pin) =>
pin.addEventListener('click', async () => {
const pinnedCategory = await getOption('pinnedCategory')
el.pinsActive.forEach((pin) =>
pin.classList.remove('category__pin--active')
)
if (pinnedCategory === id) {
await setOption('pinnedCategory', null)
} else {
await setOption('pinnedCategory', id)
el.pins.forEach((pin) => pin.classList.add('category__pin--active'))
}
})
)
technologies.forEach(
({ name, slug, confidence, version, icon, website }) => {
const technologyNode = Popup.templates.technology.cloneNode(true)
const el = {
technologies: categoryNode.querySelector('.technologies'),
iconImage: technologyNode.querySelector('.technology__icon img'),
link: technologyNode.querySelector('.technology__link'),
name: technologyNode.querySelector('.technology__name'),
version: technologyNode.querySelector('.technology__version'),
confidence: technologyNode.querySelector('.technology__confidence'),
}
el.iconImage.src = `../images/icons/${icon}`
el.link.href = `https://www.wappalyzer.com/technologies/${categorySlug}/${slug}/?utm_source=popup&utm_medium=extension&utm_campaign=wappalyzer`
el.name.textContent = name
if (confidence < 100) {
el.confidence.textContent = `${confidence}% sure`
} else {
el.confidence.remove()
}
if (version) {
el.version.textContent = version
} else {
el.version.remove()
}
el.technologies.appendChild(technologyNode)
}
)
el.detections.appendChild(categoryNode)
})
if (categorised.length === 1) {
el.detections.appendChild(Popup.templates.category.cloneNode(true))
}
Array.from(document.querySelectorAll('a')).forEach((a) =>
a.addEventListener('click', (event) => {
event.preventDefault()
event.stopImmediatePropagation()
open(a.href)
return false
})
)
i18n()
},
/**
* Show company and contact details
* @param {String} url
*/
async getPlus(url) {
const apiKey = await getOption('apiKey', '')
const el = {
loading: document.querySelector('.loading'),
panels: document.querySelector('.panels'),
empty: document.querySelector('.plus-empty'),
crawl: document.querySelector('.plus-crawl'),
error: document.querySelector('.plus-error'),
download: document.querySelector('.plus-download'),
errorMessage: document.querySelector('.plus-error__message'),
configure: document.querySelector('.plus-configure'),
credits: document.querySelector('.credits'),
creditsRemaining: document.querySelector('.credits__remaining'),
footer: document.querySelector('.footer'),
}
el.error.classList.add('plus-error--hidden')
el.download.classList.add('plus-download--hidden')
if (apiKey) {
el.loading.classList.remove('loading--hidden')
el.configure.classList.add('plus-configure--hidden')
el.footer.classList.remove('footer--hidden')
} else {
el.loading.classList.add('loading--hidden')
el.configure.classList.remove('plus-configure--hidden')
el.footer.classList.add('footer--hidden')
return
}
el.panels.classList.add('panels--hidden')
el.empty.classList.add('plus-empty--hidden')
el.crawl.classList.add('plus-crawl--hidden')
el.error.classList.add('plus-error--hidden')
while (el.panels.lastElementChild) {
el.panels.removeChild(el.panels.lastElementChild)
}
try {
const response = await fetch(
`https://api.wappalyzer.com/v2/plus/${encodeURIComponent(url)}`,
{
method: 'GET',
headers: {
'x-api-key': apiKey,
},
}
)
const data = await response.json()
if (!response.ok) {
const error = new Error()
error.data = data
error.response = response
throw error
}
const { attributes, creditsRemaining, crawl } = data
if (Popup.cache.detections.length) {
attributes.signals = attributes.signals || []
attributes.signals.technologySpend = getTechnologySpend(
Popup.cache.detections
)
}
el.creditsRemaining.textContent = parseInt(
creditsRemaining || 0,
10
).toLocaleString()
el.loading.classList.add('loading--hidden')
el.credits.classList.remove('credits--hidden')
if (crawl) {
document
.querySelector('.plus-crawl')
.classList.remove('plus-crawl--hidden')
return
}
if (!Object.keys(attributes).length) {
el.empty.classList.remove('plus-empty--hidden')
el.download.classList.remove('plus-download--hidden')
return
}
const attributeValues = {}
Object.keys(attributes).forEach((set) => {
const panel = document.createElement('div')
const header = document.createElement('div')
const content = document.createElement('div')
const table = document.createElement('table')
panel.classList.add('panel')
header.classList.add('panel__header')
content.classList.add('panel__content')
header.setAttribute(
'data-i18n',
`set${set.charAt(0).toUpperCase() + set.slice(1)}`
)
Object.keys(attributes[set]).forEach((key) => {
const value = attributes[set][key]
const tr = document.createElement('tr')
const th = document.createElement('th')
const td = document.createElement('td')
th.setAttribute(
'data-i18n',
`attribute${
key.charAt(0).toUpperCase() + key.slice(1).replace('.', '_')
}`
)
attributeValues[key] = []
if (Array.isArray(value)) {
value.forEach((value) => {
const div = document.createElement('div')
if (typeof value === 'object') {
attributeValues[key].push(value.text)
const a = document.createElement('a')
a.href = value.to
a.textContent = value.text
if (key === 'keywords') {
a.style = 'padding-right: .3rem;'
const space = document.createTextNode(' ')
td.append(a, space)
} else if (
['email', 'verifiedEmail', 'safeEmail'].includes(key)
) {
const { email, name, title } = parseEmail(value.text)
a.textContent = email
const div = document.createElement('div')
const elName = document.createElement('span')
const elTitle = document.createElement('span')
const elBreak1 = document.createElement('br')
const elBreak2 = document.createElement('br')
elName.textContent = name
elTitle.textContent = `${title}`
elTitle.className = 'light-text'
div.append(a)
if (name && name !== email) {
div.appendChild(elBreak1)
div.appendChild(elName)
if (title) {
div.appendChild(elBreak2)
div.appendChild(elTitle)
}
}
td.append(div)
} else {
div.appendChild(a)
td.appendChild(div)
}
} else if (key === 'employees') {
attributeValues[key].push(value)
const [name, title] = value.split(' -- ')
const elName = document.createElement('span')
const elTitle = document.createElement('span')
const elBreak = document.createElement('br')
elTitle.className = 'light-text'
elName.textContent = name
elTitle.textContent = title
div.appendChild(elName)
div.appendChild(elBreak)
div.appendChild(elTitle)
td.appendChild(div)
} else {
attributeValues[key].push(value)
div.textContent = value
td.appendChild(div)
}
})
} else if (key === 'companyName') {
attributeValues[key].push(value)
const strong = document.createElement('strong')
strong.textContent = value
td.appendChild(strong)
} else {
attributeValues[key].push(value)
td.textContent = value
}
if (key !== 'keywords') {
tr.appendChild(th)
}
tr.appendChild(td)
table.appendChild(tr)
})
content.appendChild(table)
panel.appendChild(header)
panel.appendChild(content)
el.panels.appendChild(panel)
})
Popup.cache.attributeValues = attributeValues
el.panels.classList.remove('panels--hidden')
el.download.classList.remove('plus-download--hidden')
} catch (error) {
Popup.log(error.data)
// eslint-disable-next-line
console.log(error)
el.errorMessage.textContent = `Sorry, something went wrong${
error.response ? ` (${error.response.status})` : ''
}. Please try again later.`
if (error.response) {
if (error.response.status === 403) {
el.errorMessage.textContent =
typeof error.data === 'string'
? error.data
: 'No access. Please check your API key.'
el.configure.classList.remove('plus-configure--hidden')
} else if (error.response.status === 429) {
el.errorMessage.textContent =
'Too many requests. Please try again in a few seconds.'
} else if (
error.response.status === 400 &&
typeof error.data === 'string'
) {
el.errorMessage.textContent = error.data
}
}
el.loading.classList.add('loading--hidden')
el.error.classList.remove('plus-error--hidden')
}
Array.from(document.querySelectorAll('.panels a')).forEach((a) =>
a.addEventListener('click', (event) => {
event.preventDefault()
open(a.href)
return false
})
)
i18n()
},
async downloadCsv(event) {
event.preventDefault()
const { csv, filename } = getCsv()
const file = URL.createObjectURL(
new Blob([csv.join('\n')], { type: 'text/csv;charset=utf-8' })
)
const granted = await promisify(chrome.permissions, 'request', {
permissions: ['downloads'],
})
if (granted) {
chrome.downloads.download({
url: file,
filename,
})
}
return false
},
}
if (/complete|interactive|loaded/.test(document.readyState)) {
Popup.init()
} else {
document.addEventListener('DOMContentLoaded', Popup.init)
}