Repository: AliasIO/demodal
Branch: master
Commit: d128b3606914
Files: 32
Total size: 53.5 KB
Directory structure:
gitextract_jysvujgv/
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── _locales/
│ ├── en/
│ │ └── messages.json
│ └── ru/
│ └── messages.json
├── background/
│ ├── background.html
│ └── background.js
├── common/
│ ├── common.css
│ └── common.js
├── content/
│ └── content.js
├── definitions/
│ ├── consent.json
│ ├── donate.json
│ ├── email.json
│ ├── message.json
│ ├── offer.json
│ ├── paywall.json
│ └── signup.json
├── inject/
│ └── inject.js
├── manifest-v2.json
├── manifest-v3.json
├── options/
│ ├── options.css
│ ├── options.html
│ └── options.js
├── package.json
├── popup/
│ ├── popup.css
│ ├── popup.html
│ └── popup.js
└── run
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .eslintignore
================================================
**/lib/*
node_modules
================================================
FILE: .eslintrc.js
================================================
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
parserOptions: {
parser: 'babel-eslint',
},
extends: [
'@nuxtjs',
'prettier',
'prettier/vue',
'plugin:prettier/recommended',
'plugin:nuxt/recommended',
'plugin:json/recommended',
],
plugins: ['prettier'],
}
================================================
FILE: .gitignore
================================================
build/*
manifest.json
node_modules
package-lock.json
!.gitkeep
Thumbs.db
Desktop.ini
*.DS_Store
*.log
._*
.idea
================================================
FILE: .prettierrc
================================================
{
"semi": false,
"arrowParens": "always",
"singleQuote": true
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 Elbert Alias
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Demodal
Demodal is a browser extension that automatically removes content blocking modals including paywalls,
discount offers, promts to sign up or enter your email address and more.
Modal dialogues such as paywalls, discount offers, cookie prompts and GDPR
banners are user-hostile interfaces that demand your attention and interrupt
your browsing experience. Demodal puts you back in control, letting you
focus on the content.
# Installation
### Google Chrome
https://chrome.google.com/webstore/detail/demodal/fjhbdkfknppikobblnjibmkmogjeffcf
Install locally:
* Clone this repository
* Run `./run manifest v3` or copy `manifest-v3.json` to `manifest.json`
* Go to `about:extensions`
* Enable 'Developer mode'
* Click 'Load unpacked'
* Select the project folder
### Mozilla Firefox
https://addons.mozilla.org/firefox/addon/demodal
Install locally:
* Clone this repository
* Run `./run manifest v2` or copy `manifest-v2.json` to `manifest.json`
* Go go `about:debugging#/runtime/this-firefox`
* Click 'Load Temporary Add-on'
* Select `manifest.json`
# Contributing
**This extension is in early development. You can help by reporting
websites with modals that didn't get blocked, or by creating your own
definitions and sharing them with us and the community. The aim is to build up
a comprehensive set of rules over time to block modals anywhere.**
Demodal is not an ad-blocker. Only create definitions for UI elements that intefere with reading of content.
When submitting a pull request, please include a screenshot of the element that's being blocked and a link to a website to test.
Every definition should clearly match one modal type (e.g. consent request or paywall). Don't create overly broad definitions (e.g. `div.modal`) that could block legimitate modals.
# Specification
## Modal types
| Type | Description |
|------------|--------------|
| `consent` | Cookie and GDPR notices.
| `donate` | Prompts to make a donation.
| `email` | Prompts to enter your email address.
| `message` | General messages and notifications.
| `offer` | Promotions and discounts.
| `paywall` | Prompts to sign up for a paid subscription.
| `signup` | Prompts to create an account.
## Definitions
Definitions are located in [`/definitions`](/definitions), file-separated by modal type. Definitions are grouped by URL pattern.
```javascript
{
"<glob> [ <glob> ... ]": { // URL pattern
// Definition
"if <function> [ <function> ... ]": { // Condition
"<selector>": "<function> [ <argument> ... ]" // Action
},
// Definition (shorthand, no condition)
"<selector>": "<function> [ <argument> ... ]" // Action
}
}
```
**Examples**
```javascript
{
"*.example.com *.example.org": {
"if $(.modal)": {
".modal": "remove" // Remove element if present
},
".modal": "addClass hide" // Remove element (shorthand)
"if defined(ModalDialog)": {
"ModalDialog.close": "call" // Call function if defined
},
"if defined(ModalDialog)": {
"ModalDialog.setClosed": "call true" // Call function with arguments
},
}
}
```
## URL pattern
URL patterns are defined as [globs](https://en.wikipedia.org/wiki/Glob_(programming)), allowing wildcards (`*`).
| Glob | Matches |
|---------------------------|---------|
| `*` | Any URL.
| `*.example.com` | Apex domain and any subdomain, e.g. `example.com`, `www.example.com`.
| `example.com example.org` | `example.com` and `example.org`.
| `*.example.com/*/about` | E.g. `www.example.com/en/about`.
## Conditions
Conditions start with `if`, followed by one or more functions. If all functions evaluate to `true`, the specified actions are run.
### Functions
| Function | Argument | Description |
|-------------|----------------------|-------------|
| `$()` | [Query selector](https://developer.mozilla.org/docs/Web/API/Document/querySelector) | Tests if an HTML element exists.
| `defined()` | JavaScript property | Tests if a JavaScript property exists.
| `sleep()` | Time in milliseconds | Returns true after the specified time has passed
## Actions
Actions are run when the condition is met, or if no condition is specified.
### Functions
| Function | Argument | Description |
|----------------|------------|-------------|
| `remove` | | Remove the HTML element.
| `removeParent` | Number | Remove parent node.
| `removeIf` | String | Remove the HTML element if its text content matches a string.
| `addClass` | Class name | Add a class.
| `removeClass` | Class name | Remove a class.
| `addStyle` | Styles | Appends CSS to the style attribute.
| `removeStyle` | | Clears the style attribute.
| `click` | | Click the HTML element.
| `call` | | Call the function. Any arguments will be passed to the function.
================================================
FILE: _locales/en/messages.json
================================================
{
"modalType": { "message": "Type" },
"blockedModals": { "message": "Blocked" },
"blockedModalsPage": { "message": "This page" },
"blockedModalsTotal": { "message": "All time" },
"modalTypes": { "message": "Modal types" },
"modalTypeOffer": { "message": "Offers" },
"modalTypeOfferHelp": { "message": "Promotions and discounts." },
"modalTypePaywall": { "message": "Paywalls" },
"modalTypePaywallHelp": { "message": "Prompts to sign up for a paid subscription." },
"modalTypeConsent": { "message": "Consent requests" },
"modalTypeConsentHelp": { "message": "Cookie and GDPR notices." },
"modalTypeSignup": { "message": "Sign-up prompts" },
"modalTypeSignupHelp": { "message": "Prompts to create an account." },
"modalTypeEmail": { "message": "Email prompts" },
"modalTypeEmailHelp": { "message": "Prompts to enter your email address." },
"modalTypeDonate": { "message": "Donation requests" },
"modalTypeDonateHelp": { "message": "Prompts to make a donation." },
"modalTypeMessage": { "message": "Messages" },
"modalTypeMessageHelp": { "message": "General messages and notifications." },
"modalStatsEmpty": { "message": "No modals blocked." },
"allowOnWebsite": { "message": "Allow modals on this website" },
"options": { "message": "Options" },
"optionModalTypesHelp": { "message": "Choose the modal types you wish to block." },
"tabBlockedModals": { "message": "Blocked modals" },
"blockedModalsHelp": { "message": "This extension is in early development. If a modal did not get blocked, please report it or create a new definition and share it." },
"tabDefinitions": { "message": "Definitions" },
"definitionsHelp": { "message": "Did a modal not get blocked? Create your own definition here and share it." },
"readMore": { "message": "Read more" }
}
================================================
FILE: _locales/ru/messages.json
================================================
{
"modalType": { "message": "Тип" },
"blockedModals": { "message": "Заблокировано" },
"blockedModalsPage": { "message": "На этой странице" },
"blockedModalsTotal": { "message": "За всё время" },
"modalTypes": { "message": "Типы всплывающих сообщений" },
"modalTypeOffer": { "message": "Акции и скидки" },
"modalTypeOfferHelp": { "message": "Предложения купить что либо." },
"modalTypePaywall": { "message": "Платный доступ" },
"modalTypePaywallHelp": { "message": "Предложения оформить платную подписку." },
"modalTypeConsent": { "message": "Уведомление о cookie" },
"modalTypeConsentHelp": { "message": "Уведомления о cookie и GDPR." },
"modalTypeSignup": { "message": "Предложения зарегистрироваться" },
"modalTypeSignupHelp": { "message": "Навязчивое предложение создать аккаунт." },
"modalTypeEmail": { "message": "Email рассылки" },
"modalTypeEmailHelp": { "message": "Навязчивое предложение указать адрес электронной почты." },
"modalTypeDonate": { "message": "Просьба о пожертвовании" },
"modalTypeDonateHelp": { "message": "Предложения сделать пожертвование (задонатить)." },
"modalTypeMessage": { "message": "Уведомления" },
"modalTypeMessageHelp": { "message": "Назойливые сообщения и уведомления." },
"modalStatsEmpty": { "message": "Ни одно всплывающие сообщение не заблокировано." },
"allowOnWebsite": { "message": "Разрешить всплывающие сообщения на этом сайте" },
"options": { "message": "Настройки" },
"optionModalTypesHelp": { "message": "Выберите, какие всплывающие сообщения вы хотите блокировать." },
"tabBlockedModals": { "message": "Заблокированные всплывающие сообщения" },
"blockedModalsHelp": { "message": "Это расширение находится на ранней стадии разработки. Если всплывающие сообщение не был заблокирован, пожалуйста, сообщите об этом или создайте новое правило для его блокировки и поделитесь им." },
"tabDefinitions": { "message": "Правила блокировки" },
"definitionsHelp": { "message": "Всплывающие сообщение не было заблокировано? Создайте свое правило блокировки здесь и поделитесь им." },
"readMore": { "message": "Читать подробнее" }
}
================================================
FILE: background/background.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="../common/common.js"></script>
<script src="background.js"></script>
</body>
</html>
================================================
FILE: background/background.js
================================================
/* globals chrome, importScripts, Common */
if (typeof importScripts !== 'undefined') {
importScripts(chrome.runtime.getURL(`common/common.js`))
}
const { modalTypes, transformDefinitions } = Common
const definitions = []
async function loadDefinitions() {
try {
definitions.push(
...(await transformDefinitions(
await Promise.all(
modalTypes.map(async (type) => ({
type,
definitions: JSON.parse(
await (
await fetch(chrome.runtime.getURL(`definitions/${type}.json`))
).text()
),
}))
)
))
)
} catch (error) {
Background.error(error)
}
}
const Background = {
async getDefinitions() {
const url = this.sender.url
// Get custom definitions
const { customDefinitions: definitionsByType } =
await chrome.storage.sync.get({
customDefinitions: {},
})
const customDefinitions = await transformDefinitions(
modalTypes.map((type) => ({
type,
definitions: definitionsByType[type] || {},
}))
)
return [...definitions, ...customDefinitions]
.filter(({ regExps }) =>
regExps.some((_regExp) => {
const regExp = new RegExp(_regExp, 'i')
return (
regExp.test(url) ||
regExp.test(`${url.replace(/^http(s)?:\/\//, 'http$1://www.')}`)
)
})
)
.map(({ definitions }) => definitions)
.flat()
},
setBadge(text) {
return chrome.action.setBadgeText({
text: String(text),
tabId: this.sender.tab.id,
})
},
log(...args) {
// eslint-disable-next-line no-console
console.log(...args)
},
error(error) {
// eslint-disable-next-line no-console
console.error(error)
},
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
const { func } = request
if (func) {
const args = request.args || []
if (request.func === 'log') {
// eslint-disable-next-line no-console
console.log('content:', ...args)
} else if (request.func === 'error') {
const [message, stack] = args
const error = new Error(`content: ${message}`)
error.stack = stack
Background.error(error)
} else {
Promise.resolve(Background[func].call({ request, sender }, ...args))
.then((response) => {
// eslint-disable-next-line no-console
console.log(`content: ${func}(${args.join(', ')})`, response)
sendResponse(response)
})
.catch((error) => Background.error(error))
}
}
return true
})
chrome.action.setBadgeBackgroundColor({
color: '#4755b3',
})
loadDefinitions().then(() =>
// eslint-disable-next-line no-console
console.log(`init ok: ${definitions.length} definitions`)
)
================================================
FILE: common/common.css
================================================
:root {
--color-primary: #667aff;
--color-primary-dark: #4755b3;
--color-primary-light: #f1f3ff;
--color-heading: #333;
--color-text: #4a4a4a;
--color-grey: #ccc;
--color-grey-light: #f4f4f4;
--color-white: #fff;
--radius: 5px;
}
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
background-color: var(--color-white);
color: var(--color-text);
direction: __MSG_@@bidi_dir__;
font-family: Helvetica, Arial, sans-serif;
font-size: .9rem;
line-height: 1.5rem;
-webkit-font-smoothing: antialiased;
}
p {
margin-bottom: 1rem;
}
a {
color: var(--color-primary);
}
input, button, textarea, select {
background-color: var(--color-white);
font: inherit;
}
input[type="text"], textarea, select {
display: block;
padding: .5rem;
border: 1px solid var(--color-grey);
border-radius: var(--radius);
margin-bottom: 1rem;
width: 100%;
}
input[type="checkbox"], input[type="radio"] {
margin-right: .5rem;
}
textarea.code {
font-family: Monaco, monospace;
font-size: .7rem;
line-height: 1.1rem;
}
h2 {
color: var(--color-heading);
font-size: .9rem;
margin-bottom: .5rem;
}
table {
border-collapse: collapse;
border-style: hidden;
border-spacing: 0;
font-size: .8rem;
width: 100%;
}
th {
color: var(--color-heading);
text-align: left;
}
th, td {
padding: .25rem .5rem;
}
th:first-child, td:first-child {
border-radius: var(--radius) 0 0 var(--radius);
}
th:last-child, td:last-child {
border-radius: 0 var(--radius) var(--radius) 0;
}
tr:first-child th,
tr:nth-child(even) td {
background: var(--color-primary-light);
}
hr {
border-color: var(--color-grey);
border-style: solid;
border-width: 1px 0 0 0;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
}
.label {
font-weight: bold;
}
.checkbox label {
display: flex;
align-items: center;
margin-bottom: .5rem;
}
.radio label {
display: inline-flex;
align-items: center;
margin-right: .5rem;
}
.help {
font-size: .8rem;
margin-bottom: .5rem;
}
.input-error {
color: #ff5353;
font-size: .8rem;
margin: -.5rem 0 .5rem 0;
}
.input-error:empty {
margin: 0;
}
.hidden {
display: none;
}
.icon {
color: #fff;
height: 1.1rem;
vertical-align: middle;
width: 1.1rem;
}
.container {
margin: 1rem;
}
.tabs {
border-bottom: 1px solid var(--color-grey);
display: flex;
}
.tab {
border-bottom: 2px solid transparent;
color: var(--color-primary);
cursor: pointer;
padding: .8rem 1rem;
margin-bottom: -1px;
}
.tab:hover {
border-bottom-color: var(--color-primary);
}
.tab--active {
border-bottom-color: var(--color-primary);
}
.alert {
background-color: #ffffcc;
color: #999900;
font-size: .8rem;
padding: 1rem;
}
.alert a, .alert a:hover, .alert a:active, .alert a:visited {
color: #999900;
}
================================================
FILE: common/common.js
================================================
/* globals chrome */
// Manifest v2 polyfill
if (chrome.runtime.getManifest().manifest_version === 2) {
chrome.action = chrome.browserAction
chrome.storage.sync = {
get: (...args) =>
new Promise((resolve) => chrome.storage.local.get(...args, resolve)),
set: (...args) =>
new Promise((resolve) => chrome.storage.local.set(...args, resolve)),
}
}
const Common = {
modalTypes: [
'offer',
'paywall',
'email',
'signup',
'consent',
'donate',
'message',
],
$:
typeof window !== 'undefined'
? document.querySelector.bind(document)
: () => {},
$$:
typeof window !== 'undefined'
? document.querySelectorAll.bind(document)
: () => {},
isObject(object) {
return typeof object === 'object' && object && !Array.isArray(object)
},
arrayify(item) {
return Array.isArray(item) ? item : [item]
},
debounce(func, wait) {
let timeout
return (...args) => {
const debounced = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(debounced, wait)
}
},
i18n() {
const elements = Common.$$('[data-i18n]')
elements.forEach((element) => {
element.textContent = chrome.i18n.getMessage(element.dataset.i18n)
})
},
capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1)
},
removeChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild)
}
},
el(name) {
return document.createElement(name)
},
getActiveTab() {
return new Promise((resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) =>
resolve(tabs[0])
)
})
},
async tokenify(string) {
const tokens = []
let bracketLevel = 0
let quote = false
let token = ''
for (const char of string.split('')) {
token += char
if (token === 'if ') {
token = ''
}
switch (char) {
case "'":
if (!bracketLevel) {
quote = !quote
if (!quote) {
tokens.push(token.replace(/(^'|'$)/g, ''))
token = ''
}
}
break
case '(':
if (!quote) {
bracketLevel++
}
break
case ')':
if (!quote) {
bracketLevel--
if (!bracketLevel) {
const [, func, arg] = token.trim().match(/([^(]+)\((.+)\)$/)
if (!Common.Functions[func]) {
throw new Error(`Function does not exist: ${func}`)
}
try {
await Common.Functions[func].call({ validateOnly: true }, arg)
} catch (error) {
throw new Error(`Invalid argument: ${arg}`)
}
tokens.push({ func, arg })
token = ''
}
}
break
}
}
if (token) {
tokens.push(token)
}
return tokens
},
async transformDefinitions(definitionsByType) {
const definitionsByGlob = []
for (const { type, definitions } of definitionsByType) {
if (!Common.isObject(definitions)) {
throw new TypeError(
`Unexpected definitions type, expected object: ${definitions}`
)
}
for (const glob of Object.keys(definitions)) {
definitionsByGlob[glob] = definitionsByGlob[glob] || {
glob,
regExps: Common.globToRegExp(glob),
definitions: [],
}
definitionsByGlob[glob].definitions.push(
...(
await Promise.all(
Common.arrayify(definitions[glob]).map((definition) => {
try {
if (!Common.isObject(definition)) {
throw new TypeError(
`Unexpected definition type, expected object: ${definition}`
)
}
return Promise.all(
Object.keys(definition).map(async (key) => {
const conditions = key.startsWith('if ')
? await Common.tokenify(key)
: null
if (conditions && !conditions.length) {
throw new Error(`Invalid condition: ${key}`)
}
let actions = []
if (conditions) {
if (!Common.isObject(definition[key])) {
throw new TypeError(
`Invalid actions type, expected object: ${definition[key]}`
)
}
actions = Object.keys(definition[key]).map(
(selector) => ({
selector,
action: definition[key][selector],
})
)
} else {
actions = [{ selector: key, action: definition[key] }]
}
actions = await Promise.all(
actions.map(async ({ selector, action }) => {
if (typeof action !== 'string') {
throw new TypeError(
`Unexpected action type, expected string`
)
}
const [func, ...splitArgs] = action.split(' ')
const args = await Common.tokenify(
splitArgs.join(' ')
)
return { selector, func, args }
})
)
actions.forEach(({ selector, func, args }) => {
try {
Common.$(selector)
} catch (error) {
throw new Error(
`Invalid action selector: ${selector}`
)
}
if (!Common.Actions[func]) {
throw new Error(
`Invalid action function: ${func} (${selector})`
)
}
})
return { type, conditions: conditions || [], actions }
})
)
} catch (error) {
throw new Error(`${error.message || error} in ${glob}`)
}
})
)
).flat()
)
}
}
return Object.values(definitionsByGlob).flat()
},
globToRegExp(glob) {
const globs = glob.split(' ')
return globs.map((glob) => {
try {
if (glob !== '*') {
if (!glob.includes('.')) {
throw new Error('Invalid glob')
}
// eslint-disable-next-line no-new
new URL(`https://${glob.replace('*', 'test')}`)
}
return new RegExp(
glob === '*'
? '^https?://.+'
: `^https?://${glob.replace('.', '\\.').replace('*', '[^./]+')}\\b`
).source
} catch (error) {
throw new Error(`Invalid URL pattern: ${glob}`)
}
})
},
// Run a script in the context of the page
inject(func, ...args) {
return new Promise((resolve) => {
const uid = Math.floor(Math.random() * 900000) + 100000
const script = Common.el('script')
script.src = chrome.runtime.getURL('inject/inject.js')
script.dataset.demodal = 'true'
script.onload = () => {
const receiveMessage = ({ data: { demodalResponse } }) => {
if (!demodalResponse || demodalResponse.uid !== uid) {
return
}
window.removeEventListener('message', receiveMessage)
script.remove()
resolve(demodalResponse.message)
}
window.addEventListener('message', receiveMessage)
window.postMessage({ demodalRequest: { func, args, uid } })
}
document.body.append(script)
})
},
Actions: {
remove() {
this.remove()
return true
},
removeParent(level = 1) {
let node = this
for (let index = 0; index < level; index++) {
node = node ? node.parentNode : null
}
if (node) {
node.remove()
return true
} else {
return false
}
},
removeIf(...args) {
if (
args.every((string) =>
this.textContent.toLowerCase().includes(string.toLowerCase().trim())
)
) {
this.remove()
return true
}
return false
},
addClass(...args) {
this.classList.add(...args)
return true
},
removeClass(...args) {
if (args[0] === '*') {
this.className = ''
} else {
this.classList.remove(...args)
}
return true
},
addStyle(...args) {
this.style = `${this.style}; ${args[0]}`
return true
},
removeStyle() {
this.style = ''
return true
},
click() {
this.click()
return true
},
call(...args) {
Common.inject('call', ...args)
return true
},
},
Functions: {
$(selector) {
try {
return !!Common.$(selector)
} catch (error) {
throw new Error(`Invalid selector: ${selector}`)
}
},
async defined(...args) {
if (this.validateOnly) {
return true
}
return await Common.inject('defined', ...args)
},
sleep(ms = 0) {
if (this.validateOnly) {
return true
}
return new Promise((resolve) =>
setTimeout(() => resolve(true), parseInt(ms, 10))
)
},
},
Background: {
call(func, ...args) {
return new Promise((resolve, reject) =>
chrome.runtime.sendMessage({ func, args }, (response) =>
chrome.runtime.lastError
? reject(new Error(chrome.runtime.lastError.message))
: resolve(response)
)
)
},
},
Content: {
call(func, ...args) {
return new Promise((resolve, reject) =>
Common.getActiveTab().then((tab) =>
chrome.tabs.sendMessage(tab.id, { func, args }, (response) =>
chrome.runtime.lastError
? reject(new Error(chrome.runtime.lastError.message))
: resolve(response)
)
)
)
},
},
}
================================================
FILE: content/content.js
================================================
/* eslint-env browser */
/* globals chrome, Common */
const { $, debounce, Background, Actions, Functions } = Common
const definitions = []
const blockedModals = {}
// Functions that can be called from the popup script
const Content = {
getBlockedModals() {
return blockedModals
},
getUrl() {
return window.location.href
},
reload() {
window.location.reload()
},
async isAllowed(url = window.location.href) {
// Check if hostname is in allow list
const { allowList } = await chrome.storage.sync.get({
allowList: [],
})
let { hostname } = new URL(url)
hostname = hostname.replace(/^www\./, '')
return allowList.includes(hostname)
},
}
// Log messages in the background script console
function log(...messages) {
const error = messages[0]
if (error instanceof Error) {
Background.call('error', error.toString(), error.stack)
} else {
Background.call('log', ...messages)
}
}
const run = async () => {
try {
if (await Content.isAllowed()) {
return
}
await Promise.all(
definitions.map(async (definition) => {
const { type, conditions, actions, completed } = definition
if (
completed ||
!(
await Promise.all(
conditions.map(({ func, arg }) => Functions[func](arg))
)
).every((result) => result)
) {
return
}
let found = false
actions.forEach(({ selector, func, args }) => {
let success = false
switch (func) {
case 'call':
success = Actions[func](selector, ...args)
break
default:
// eslint-disable-next-line no-case-declarations
const node = $(selector)
if (node) {
success = Actions[func].call(node, ...args)
}
}
if (success) {
log(`action: ${selector}: ${func}(${args.join(', ')})`)
}
found = found || success
})
if (found) {
definition.completed = true
blockedModals[type] = (blockedModals[type] || 0) + 1
Background.call(
'setBadge',
Object.values(blockedModals).reduce((sum, value) => sum + value, 0)
)
// Update all-time totals
chrome.storage.sync
.get({
blockedModals: {},
})
.then(({ blockedModals }) => {
blockedModals[type] = (blockedModals[type] || 0) + 1
chrome.storage.sync.set({ blockedModals })
})
}
})
)
} catch (error) {
log(error)
}
}
// Listen for messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
const { func } = request
if (func) {
const args = request.args || []
Promise.resolve(Content[func].call({ request, sender }, ...args))
.then((response) => {
// eslint-disable-next-line no-console
log(`popup: ${func}(${args.join(', ')})`, response)
sendResponse(response)
})
.catch((error) => log(error))
}
return true
})
//
;(async () => {
try {
definitions.push(...(await Background.call('getDefinitions')))
const runDebounced = debounce(() => run(), 500)
const mutationObserver = new MutationObserver((mutations) => {
// Avoid infinite loop if mutation was done by us
if (
mutations.every((mutation) =>
Array.from(mutation.addedNodes).every((node) => node.dataset.demodal)
)
) {
return
}
runDebounced()
})
mutationObserver.observe(document.body, { subtree: true, childList: true })
run()
} catch (error) {
log(error)
}
})()
================================================
FILE: definitions/consent.json
================================================
{
"*": {
"#cookie-law-info-bar": "remove",
"#moove_gdpr_cookie_info_bar": "remove",
"if defined(BorlabsCookie)": {
"BorlabsCookie.hideCookieBox": "call"
},
"#cookie-banner": "remove",
".cookie-banner": "remove",
"#onetrust-consent-sdk": "remove",
"#CybotCookiebotDialogBodyButtonDecline": "click",
".cookie-bar__close": "click",
"[aria-label='cookieconsent']": "remove",
".cookie-warning-modal": "remove",
".cookie-consent": "remove",
"#cookie-notice": "remove",
".cookie-notice-big": "remove",
"#cookie-bar": "remove",
"if $(#didomi-host)": {
"#didomi-host": "remove",
"body": "removeClass didomi-popup-open"
},
"#cookieBanner": "remove",
"#cconsent-bar": "remove",
".CookieConsent": "remove",
"#gdpr-banner": "remove",
"#gdpr-cookie-message": "remove",
".cookie-notice": "remove",
".cc-banner": "remove",
".js-cookie-consent-overlay": "remove",
"#ccc": "remove",
"#iubenda-cs-banner": "remove",
".wppopups-whole": "removeIf 'cookies'",
".gdpr-disclaimer": "remove",
"._gdprDisclaimer": "remove",
".ce-cookie-banner": "remove",
".cookie-msg": "remove",
".cookie-panel": "remove",
".cookie-popup": "remove",
"#cookie-popup-wrapper": "remove",
"[data-component='CookieBanner']": "remove",
".t-consentPrompt": "remove",
"#privacy-consent": "remove",
"if $(#acceptationCMPWall)": {
"#acceptationCMPWall": "remove",
"body": "removeStyle"
},
".cookies_message": "remove",
"#cookieConsent": "remove",
".cookie-policy": "remove",
"#cookieNoticeAlert": "remove"
},
"*.stackexchange.com *.superuser.com *.stackoverflow.com *.mathoverflow.net *.serverfault.com *.askubuntu.com *.stackapps.com": {
".js-consent-banner": "remove"
},
"*.deepl.com": {
"#dl_cookieBanner": "remove"
},
"*.lego.com": {
".cookies-used-notice": "remove"
},
"*.redhat.com": {
"#truste-consent-track": "remove",
".redhat-cookie-banner": "remove"
},
"*.openweathermap.org": {
"#stick-footer-panel": "remove"
}
}
================================================
FILE: definitions/donate.json
================================================
{
"*.theguardian.com": {
".site-message--banner": "remove",
"#bottom-banner": "removeIf 'Support the Guardian'"
},
"*.science.org": {
".alert-donation": "remove"
}
}
================================================
FILE: definitions/email.json
================================================
{
"*": {
"if $(.elementor-popup-modal)": {
".elementor-popup-modal": "remove",
"body": "removeClass dialog-prevent-scroll"
},
"if $(.sgpb-popup-dialog-main-div-wrapper input[type='email'])": {
".sgpb-popup-dialog-main-div-wrapper": "remove"
},
"if $([aria-modal='true'] .klaviyo-close-form)": {
"[aria-modal='true'] .klaviyo-close-form": "click"
},
"#shopify-section-newsletter-popup .modal__close": "click",
".close-newsletter": "click",
".cp-popup-container": "removeIf 'subscribe'",
".js_modal_exit_intent": "removeIf 'subscribe'"
},
"*.npr.org": {
".newsletter-stickybar": "remove"
},
"*.redhat.com": {
".subscribe-sidebar": "remove"
}
}
================================================
FILE: definitions/message.json
================================================
{
"*": {
"if $([id^='sp_message_container_'])": {
"[id^='sp_message_container_']": "remove",
"html": "removeClass sp-message-open"
}
},
"*.heraldsun.com.au": {
".DialogBox": "remove"
},
"*.simplywall.st": {
"if $([data-cy-id='careers-upsell'])": {
"[data-cy-id='careers-upsell']": "removeParent"
}
}
}
================================================
FILE: definitions/offer.json
================================================
{
"*.forbes.com": {
"[external-event='offer-close-modal']": "click"
},
"*.theconversation.com": {
".promo": "remove"
}
}
================================================
FILE: definitions/paywall.json
================================================
{
"*.nytimes.com": {
"if $(#gateway-content)": {
"#gateway-content": "remove",
"#app > div > div": "removeClass *",
"#app > div > div > div:last-child": "remove",
"#site-content": "removeStyle"
}
},
"*.newyorker.com": {
".paywall-bar": "remove",
".paywall-modal": "remove"
},
"*.wired.com": {
".persistent-bottom": "remove"
},
"*.washingtonpost.com": {
"if $(.paywall-overlay) sleep(1000)": {
".paywall-overlay": "remove",
"html": "removeStyle",
"body": "addStyle 'overflow: inherit; position: inherit;'"
},
".softwall-overlay": "remove",
"[id^='softwall-']": "remove"
},
"*.simplywall.st": {
"if $(#modal-container)": {
"#modal-container": "remove",
"#root": "addStyle filter: none"
}
},
"*.theguardian.com": {
"#bottom-banner": "removeIf 'Start free trial'"
},
"*.bloomberg.com": {
"if $(#fortress-paywall-container-root)": {
"#fortress-paywall-container-root": "remove",
"body": "addStyle overflow: auto"
}
},
"*.nikkei.com": {
".pw-widget--benefit-pop-up .pianoj-ribbon-close": "click"
},
"*.telegraph.co.uk": {
".martech-modal-component-overlay": "remove"
},
"*.nationalgeographic.com": {
"if $(.EmailStickyFooter__Modal)": {
".EmailStickyFooter__Modal": "remove",
".Scroll--locked": "removeClass Scroll--locked",
"body": "removeStyle"
}
}
}
================================================
FILE: definitions/signup.json
================================================
{
"*": {
".wbounce-modal": "removeIf 'newsletter'",
".leadinModal": "remove",
".wppopups-whole": "removeIf 'sign up'",
".exit-intent": "removeIf 'sign up'",
".exit-intent-modal": "remove",
"[id^='smsbump-form']": "remove",
"#ouibounce-modal": "remove"
},
"*.smh.com.au *.theage.com.au": {
"[data-testid='registration-prompt']": "remove",
"#registrationWall": "remove"
},
"*.ishka.com.au": {
"if $(.sign_up_karma_club_popup) defined(CloseClubPoup)": {
"CloseClubPoup": "call"
}
},
"*.nytimes.com": {
".MAG_web_all_Monthly-Sale-dock": "remove"
},
"*.theatlantic.com": {
"#paywall[data-category^='nudge']": "remove"
},
"*.bloomberg.com": {
"#fortress-preblocked-container-root": "remove"
},
"*.ieee.org": {
".lightbox-popup": "removeIf 'create an account'"
},
"*.afr.com": {
"if $([data-testid='SubscriptionPrompt-close'])": {
"[data-testid='SubscriptionPrompt-close']": "click"
}
},
"*.pinterest.com *.pinterest.com.au": {
"if $([data-test-id='giftWrap'])": {
"[data-test-id='giftWrap']": "remove",
"body": "removeStyle"
}
},
"*.vic.gov.au": {
"#subscribe-banner-react": "remove"
},
"*.nature.com": {
".c-site-messages--nature-briefing-email-variant": "remove"
},
"*.boredpanda.com": {
"if $(#subscribe-before-leaving)": {
"#subscribe-before-leaving": "remove",
"#modal-backdrop": "remove",
"body": "removeClass modal-open"
}
},
"*.bbc.com": {
"if $(.tp-modal-open)": {
"body": "removeClass tp-modal-open",
".tp-modal": "remove",
".tp-backdrop": "remove"
}
},
"*.ibtimes.com": {
".grwf2_backdrop": "remove",
".wf2-popover": "remove"
},
"vk.com": {
"#page_bottom_banners_root": "remove",
"if $(.UnauthActionBox)": {
".UnauthActionBox__close": "click"
}
}
}
================================================
FILE: inject/inject.js
================================================
/* eslint-env browser */
;(function () {
try {
const chainToProp = (chain) => {
return chain
.split('.')
.reduce(
(value, method) =>
value &&
value instanceof Object &&
Object.prototype.hasOwnProperty.call(value, method)
? value[method]
: undefined,
window
)
}
const Functions = {
defined(chain) {
return chainToProp(chain) !== undefined
},
call(chain, ...args) {
chainToProp(chain)?.(...args)
},
}
const receiveMessage = ({ data: { demodalRequest } }) => {
if (!demodalRequest) {
return
}
const { func, args, uid } = demodalRequest
removeEventListener('message', receiveMessage)
postMessage({
demodalResponse: {
uid,
message:
func && Functions[func] ? Functions[func](...(args || [])) : false,
},
})
}
addEventListener('message', receiveMessage)
} catch (error) {
// Fail quietly
}
})()
================================================
FILE: manifest-v2.json
================================================
{
"name": "Demodal - Block modals and overlays",
"description": "Demodal automatically removes content blocking modals including paywalls, discount offers, promts to sign up or enter your email address and more.",
"version": "1.0.3",
"manifest_version": 2,
"default_locale": "en",
"background": {
"page": "background/background.html"
},
"content_scripts": [
{
"matches": ["https://*/*", "http://*/*"],
"js": [
"common/common.js",
"content/content.js"
]
}
],
"permissions": ["storage", "activeTab"],
"browser_action": {
"default_popup": "popup/popup.html",
"default_icon": {
"128": "images/icon-128.png",
"256": "images/icon-256.png"
}
},
"options_ui":{
"page": "options/options.html",
"open_in_tab": true
},
"icons": {
"128": "images/icon-128.png",
"256": "images/icon-256.png"
}
}
================================================
FILE: manifest-v3.json
================================================
{
"name": "Demodal - Block modals and overlays",
"description": "Demodal automatically removes content blocking modals including paywalls, discount offers, email address prompts and more.",
"version": "1.0.3",
"manifest_version": 3,
"default_locale": "en",
"background": {
"service_worker": "background/background.js"
},
"content_scripts": [
{
"matches": ["https://*/*", "http://*/*"],
"js": [
"common/common.js",
"content/content.js"
]
}
],
"permissions": ["storage", "activeTab", "scripting"],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"128": "images/icon-128.png",
"256": "images/icon-256.png"
}
},
"options_page": "options/options.html",
"icons": {
"128": "images/icon-128.png",
"256": "images/icon-256.png"
}
}
================================================
FILE: options/options.css
================================================
.container {
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
================================================
FILE: options/options.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title data-i18n="options"></title>
<link rel="stylesheet" href="../common/common.css">
<link rel="stylesheet" href="options.css">
</head>
<body>
<div class="container">
<div class="card">
<div class="card__content">
<h2 data-i18n="modalTypes"></h2>
<p data-i18n="optionModalTypesHelp"><p>
<div id="modal-types"></div>
</div>
</div>
</div>
<script src="../common/common.js"></script>
<script src="options.js"></script>
</body>
</html>
================================================
FILE: options/options.js
================================================
/* globals chrome, Common */
const { $, i18n, el, capitalize, modalTypes } = Common
i18n()
//
;(async () => {
const { optionBlockModalTypes } = await chrome.storage.sync.get({
optionBlockModalTypes: modalTypes.reduce(
(options, type) => ({ ...options, [type]: true }),
{}
),
})
modalTypes.forEach((type) => {
const div1 = el('div')
const label = el('label')
const checkbox = el('input')
const div2 = el('div')
div1.className = 'checkbox'
checkbox.type = 'checkbox'
checkbox.checked = optionBlockModalTypes[type]
checkbox.addEventListener('change', () => {
optionBlockModalTypes[type] = checkbox.checked
chrome.storage.sync.set({ optionBlockModalTypes })
})
div2.textContent = chrome.i18n.getMessage(
`modalType${capitalize(type)}Help`
)
div2.className = 'help'
label.append(
checkbox,
chrome.i18n.getMessage(`modalType${capitalize(type)}`)
)
div1.append(label, div2)
$('#modal-types').append(div1)
})
})()
================================================
FILE: package.json
================================================
{
"devDependencies": {
"@nuxtjs/eslint-config": "^3.1.0",
"@nuxtjs/eslint-module": "^2.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.13.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-json": "^2.1.2",
"eslint-plugin-nuxt": "^1.0.0",
"eslint-plugin-prettier": "^3.1.4",
"prettier": "^2.1.2"
}
}
================================================
FILE: popup/popup.css
================================================
body {
width: 500px;
}
.header {
background-color: var(--color-primary);
display: flex;
align-items: center;
padding: 1rem;
}
.header__logo {
margin-right: .8rem;
width: 24px;
}
.header__title {
color: var(--color-white);
font-size: .9rem;
text-transform: uppercase;
}
.header__links {
text-align: right;
flex: 1 0;
}
.content {
opacity: .5;
}
.content.visible {
opacity: 1;
}
#input-definitions {
height: 300px;
}
#input-debug {
height: 300px;
}
================================================
FILE: popup/popup.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="../common/common.css">
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="header">
<img class="header__logo" src="../images/icon-mono-256.png" alt="" />
<h1 class="header__title">Demodal</h1>
<div class="header__links">
<a class="link-options" href="#">
<svg class="icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
</svg>
</a>
</div>
</div>
<div class="tabs">
<div class="tab tab--active" data-tab="blocked-modals" data-i18n="tabBlockedModals"></div>
<div class="tab" data-tab="definitions" data-i18n="tabDefinitions"></div>
</div>
<div class="content">
<div data-tab-content="blocked-modals">
<div class="alert">
<span data-i18n="blockedModalsHelp"></span>
<a href="https://github.com/AliasIO/demodal/blob/master/README.md" target="_blank" data-i18n="readMore"></a>
</div>
<div class="if-connected">
<div id="options" class="container">
<div class="checkbox">
<label>
<input id="input-allowed" type="checkbox"> <span data-i18n="allowOnWebsite"></span>
</label>
</div>
</div>
<hr />
<div id="blocked-page" class="container">
<h2 data-i18n="blockedModalsPage"></h2>
<div id="blocked-page__missing" class="hidden" data-i18n="modalStatsEmpty"></div>
<table id="blocked-page__stats">
<thead>
<tr>
<th width="50%">
<span data-i18n="modalType"></span>
</th>
<th width="50%">
<span data-i18n="blockedModals"></span>
</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="blocked-total" class="container">
<h2 data-i18n="blockedModalsTotal"></h2>
<div id="blocked-total__missing" class="hidden" data-i18n="modalStatsEmpty"></div>
<table id="blocked-total__stats">
<thead>
<tr>
<th width="50%">
<span data-i18n="modalType"></span>
</th>
<th width="50%">
<span data-i18n="blockedModals"></span>
</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="hidden" data-tab-content="definitions">
<div class="container">
<p>
<span data-i18n="definitionsHelp"></span>
<a href="https://github.com/AliasIO/demodal/blob/master/README.md" target="_blank" data-i18n="readMore"></a>
</p>
<select id="input-modal-types"></select>
<textarea id="input-definitions" class="code"></textarea>
<textarea id="input-debug" class="code hidden" readonly></textarea>
<div id="errors-definitions" class="input-error"></div>
<div class="row">
<div class="radio">
<label>
<input type="radio" name="input-mode" value="edit" checked /> Edit
</label>
<label>
<input type="radio" name="input-mode" value="debug" /> Debug
</label>
</div>
<div class="text-right">
<a href="#" class="reload">Reload page</a>
</div>
</div>
</div>
</div>
</div>
<script src="../common/common.js"></script>
<script src="popup.js"></script>
</body>
</html>
================================================
FILE: popup/popup.js
================================================
/* globals chrome, Common */
const {
$,
$$,
i18n,
capitalize,
removeChildren,
el,
debounce,
modalTypes,
transformDefinitions,
Content,
} = Common
function renderTotals(prefix, totals) {
let any = false
removeChildren($(`${prefix}__stats tbody`))
modalTypes.forEach((type) => {
const tr = el('tr')
const td1 = el('td')
const td2 = el('td')
if (totals[type]) {
td1.textContent = chrome.i18n.getMessage(`modalType${capitalize(type)}`)
td2.textContent = totals[type]
tr.appendChild(td1)
tr.appendChild(td2)
$(`${prefix}__stats tbody`).appendChild(tr)
any = true
}
})
if (any) {
$(`${prefix}__stats`).classList.remove('hidden')
$(`${prefix}__missing`).classList.add('hidden')
}
}
i18n()
//
;(async () => {
$('.content').classList.remove('visible')
$(`#blocked-page__stats`).classList.add('hidden')
$(`#blocked-page__missing`).classList.remove('hidden')
$(`#blocked-total__stats`).classList.add('hidden')
$(`#blocked-total__missing`).classList.remove('hidden')
$$('.tab').forEach((tab) =>
tab.addEventListener('click', (e) => {
$$('.tab').forEach((tab) => tab.classList.remove('tab--active'))
e.target.classList.add('tab--active')
$$('[data-tab-content]').forEach((tabContent) =>
tabContent.classList.add('hidden')
)
$(`[data-tab-content='${e.target.dataset.tab}']`).classList.remove(
'hidden'
)
})
)
// Blocked modals tab
$('.link-options').addEventListener('click', (e) => {
e.preventDefault()
chrome.runtime.openOptionsPage()
})
// Show all-time totals
const { blockedModals: totals } = await chrome.storage.sync.get({
blockedModals: {},
})
renderTotals('#blocked-total', totals)
let connected = true
try {
// Show page totals
const blockedModals = await Content.call('getBlockedModals')
renderTotals('#blocked-page', blockedModals)
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
connected = false
}
// Show page specific content only if we have a content script connection
$$('.if-connected').forEach((element) =>
element.classList[connected ? 'remove' : 'add']('hidden')
)
if (connected) {
// Add/remove hostnames to allow list
const allowed = await Content.call('isAllowed')
$('#input-allowed').checked = allowed
$('#input-allowed').addEventListener('click', async (el) => {
const { allowList } = await chrome.storage.sync.get({
allowList: [],
})
const url = await Content.call('getUrl')
let { hostname } = new URL(url)
hostname = hostname.replace(/^www\./, '')
if (el.target.checked) {
allowList.push(hostname)
} else {
const index = allowList.findIndex((_hostname) => _hostname === hostname)
if (index !== -1) {
allowList.splice(index, 1)
}
}
chrome.storage.sync.set({ allowList })
Content.call('reload')
})
}
// Definitions tab
// Modal type select
modalTypes.forEach((type) => {
const option = el('option')
option.value = type
option.textContent = chrome.i18n.getMessage(`modalType${capitalize(type)}`)
$('#input-modal-types').append(option)
})
$('#input-modal-types').addEventListener('change', (event) => {
const modalType = event.target.value
$('#errors-definitions').textContent = ''
chrome.storage.sync
.get({
customDefinitions: {},
})
.then(
({ customDefinitions }) =>
($('#input-definitions').value = JSON.stringify(
customDefinitions[modalType] || {},
null,
2
))
)
})
$('#input-modal-types').dispatchEvent(new Event('change'))
// Edit / debug toggle
$$('input[name="input-mode"]').forEach((el) =>
el.addEventListener('change', (event) => {
const debug = event.target.value === 'debug'
$('#input-definitions').classList[debug ? 'add' : 'remove']('hidden')
$('#input-debug').classList[debug ? 'remove' : 'add']('hidden')
})
)
// Reload page
$('.reload').addEventListener('click', () => Content.call('reload'))
// Format JSON on blur
$('#input-definitions').addEventListener('blur', (event) => {
const json = event.target.value || '{}'
try {
const definitions = JSON.parse(json)
event.target.value = JSON.stringify(definitions, null, 2)
const selectedType = $('#input-modal-types').value
chrome.storage.sync
.get({
customDefinitions: modalTypes.reduce(
(definitions, type) => ({ ...definitions, [type]: {} }),
{}
),
})
.then(({ customDefinitions }) =>
chrome.storage.sync.set({
customDefinitions: {
...customDefinitions,
[selectedType]: definitions,
},
})
)
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
})
// Validate JSON on change
$('#input-definitions').addEventListener(
'input',
debounce((event) => {
const json = event.target.value || '{}'
try {
const definitionsByType = [
{
type: 'offers',
definitions: JSON.parse(json),
},
]
const definitions = transformDefinitions(definitionsByType)
$('#input-debug').textContent = JSON.stringify(definitions, null, 2)
$('#errors-definitions').textContent = ''
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
$('#errors-definitions').textContent = error.message || error.toString()
}
}, 500)
)
$('.content').classList.add('visible')
})()
================================================
FILE: run
================================================
#!/bin/bash
case $1 in
manifest)
if [[ -f manifest-$2.json ]]; then
cat manifest-$2.json > manifest.json
echo Switched to manifest $2
else
echo Invalid argument $2
fi
;;
build)
version=$2
if [ -z "$version" ]; then
echo "No version specified"
exit
fi
sed -i '' -r "s/\"version\": \".+\"/\"version\": \"$2\"/" manifest-v2.json
sed -i '' -r "s/\"version\": \".+\"/\"version\": \"$2\"/" manifest-v3.json
find . -name '.DS_Store' -type f -delete
files="_locales background common content definitions images inject options popup manifest.json"
cp manifest.json manifest.json.bak
cat manifest-v2.json > manifest.json
rm build/*
zip -r build/demodal-v2.zip $files
cat manifest-v3.json > manifest.json
zip -r build/demodal-v3.zip $files
mv manifest.json.bak manifest.json
;;
*)
echo Invalid argument $1
;;
esac
gitextract_jysvujgv/ ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── _locales/ │ ├── en/ │ │ └── messages.json │ └── ru/ │ └── messages.json ├── background/ │ ├── background.html │ └── background.js ├── common/ │ ├── common.css │ └── common.js ├── content/ │ └── content.js ├── definitions/ │ ├── consent.json │ ├── donate.json │ ├── email.json │ ├── message.json │ ├── offer.json │ ├── paywall.json │ └── signup.json ├── inject/ │ └── inject.js ├── manifest-v2.json ├── manifest-v3.json ├── options/ │ ├── options.css │ ├── options.html │ └── options.js ├── package.json ├── popup/ │ ├── popup.css │ ├── popup.html │ └── popup.js └── run
SYMBOL INDEX (39 symbols across 5 files)
FILE: background/background.js
function loadDefinitions (line 11) | async function loadDefinitions() {
method getDefinitions (line 33) | async getDefinitions() {
method setBadge (line 64) | setBadge(text) {
method log (line 71) | log(...args) {
method error (line 76) | error(error) {
FILE: common/common.js
method isObject (line 35) | isObject(object) {
method arrayify (line 39) | arrayify(item) {
method debounce (line 43) | debounce(func, wait) {
method i18n (line 59) | i18n() {
method capitalize (line 67) | capitalize(string) {
method removeChildren (line 71) | removeChildren(element) {
method el (line 77) | el(name) {
method getActiveTab (line 81) | getActiveTab() {
method tokenify (line 89) | async tokenify(string) {
method transformDefinitions (line 156) | async transformDefinitions(definitionsByType) {
method globToRegExp (line 263) | globToRegExp(glob) {
method inject (line 289) | inject(func, ...args) {
method remove (line 322) | remove() {
method removeParent (line 327) | removeParent(level = 1) {
method removeIf (line 342) | removeIf(...args) {
method addClass (line 355) | addClass(...args) {
method removeClass (line 360) | removeClass(...args) {
method addStyle (line 369) | addStyle(...args) {
method removeStyle (line 374) | removeStyle() {
method click (line 379) | click() {
method call (line 384) | call(...args) {
method $ (line 392) | $(selector) {
method defined (line 399) | async defined(...args) {
method sleep (line 406) | sleep(ms = 0) {
method call (line 418) | call(func, ...args) {
method call (line 430) | call(func, ...args) {
FILE: content/content.js
method getBlockedModals (line 12) | getBlockedModals() {
method getUrl (line 16) | getUrl() {
method reload (line 20) | reload() {
method isAllowed (line 24) | async isAllowed(url = window.location.href) {
function log (line 39) | function log(...messages) {
FILE: inject/inject.js
method defined (line 20) | defined(chain) {
method call (line 23) | call(chain, ...args) {
FILE: popup/popup.js
function renderTotals (line 16) | function renderTotals(prefix, totals) {
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (61K chars).
[
{
"path": ".editorconfig",
"chars": 207,
"preview": "# editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_"
},
{
"path": ".eslintignore",
"chars": 22,
"preview": "**/lib/*\nnode_modules\n"
},
{
"path": ".eslintrc.js",
"chars": 327,
"preview": "module.exports = {\n root: true,\n env: {\n browser: true,\n node: true,\n },\n parserOptions: {\n parser: 'babel-"
},
{
"path": ".gitignore",
"chars": 114,
"preview": "build/*\nmanifest.json\nnode_modules\npackage-lock.json\n\n!.gitkeep\n\nThumbs.db\nDesktop.ini\n*.DS_Store\n*.log\n._*\n.idea\n"
},
{
"path": ".prettierrc",
"chars": 70,
"preview": "{\n \"semi\": false,\n \"arrowParens\": \"always\",\n \"singleQuote\": true\n}\n"
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2021 Elbert Alias\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 4940,
"preview": "\n# Demodal\n\nDemodal is a browser extension that automatically removes content blocking modals including paywalls, \ndisco"
},
{
"path": "_locales/en/messages.json",
"chars": 1808,
"preview": "{\n \"modalType\": { \"message\": \"Type\" },\n \"blockedModals\": { \"message\": \"Blocked\" },\n \"blockedModalsPage\": { \"message\":"
},
{
"path": "_locales/ru/messages.json",
"chars": 2127,
"preview": "{\n \"modalType\": { \"message\": \"Тип\" },\n \"blockedModals\": { \"message\": \"Заблокировано\" },\n \"blockedModalsPage\": { \"mess"
},
{
"path": "background/background.html",
"chars": 203,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n\n\t\t<title></title>\n </head>\n <body>\n <script src=\"../com"
},
{
"path": "background/background.js",
"chars": 2837,
"preview": "/* globals chrome, importScripts, Common */\n\nif (typeof importScripts !== 'undefined') {\n importScripts(chrome.runtime."
},
{
"path": "common/common.css",
"chars": 2864,
"preview": ":root {\n --color-primary: #667aff;\n --color-primary-dark: #4755b3;\n --color-primary-light: #f1f3ff;\n --color-heading"
},
{
"path": "common/common.js",
"chars": 10652,
"preview": "/* globals chrome */\n\n// Manifest v2 polyfill\nif (chrome.runtime.getManifest().manifest_version === 2) {\n chrome.action"
},
{
"path": "content/content.js",
"chars": 3816,
"preview": "/* eslint-env browser */\n/* globals chrome, Common */\n\nconst { $, debounce, Background, Actions, Functions } = Common\n\nc"
},
{
"path": "definitions/consent.json",
"chars": 2125,
"preview": "{\n \"*\": {\n \"#cookie-law-info-bar\": \"remove\",\n \"#moove_gdpr_cookie_info_bar\": \"remove\",\n \"if defined(BorlabsCoo"
},
{
"path": "definitions/donate.json",
"chars": 186,
"preview": "{\n \"*.theguardian.com\": {\n \".site-message--banner\": \"remove\",\n \"#bottom-banner\": \"removeIf 'Support the Guardian'"
},
{
"path": "definitions/email.json",
"chars": 725,
"preview": "{\n \"*\": {\n \"if $(.elementor-popup-modal)\": {\n \".elementor-popup-modal\": \"remove\",\n \"body\": \"removeClass di"
},
{
"path": "definitions/message.json",
"chars": 353,
"preview": "{\n \"*\": {\n \"if $([id^='sp_message_container_'])\": {\n \"[id^='sp_message_container_']\": \"remove\",\n \"html\": \""
},
{
"path": "definitions/offer.json",
"chars": 137,
"preview": "{\n \"*.forbes.com\": {\n \"[external-event='offer-close-modal']\": \"click\"\n },\n \"*.theconversation.com\": {\n \".promo\""
},
{
"path": "definitions/paywall.json",
"chars": 1443,
"preview": "{\n \"*.nytimes.com\": {\n \"if $(#gateway-content)\": {\n \"#gateway-content\": \"remove\",\n \"#app > div > div\": \"re"
},
{
"path": "definitions/signup.json",
"chars": 1895,
"preview": "{\n \"*\": {\n \".wbounce-modal\": \"removeIf 'newsletter'\",\n \".leadinModal\": \"remove\",\n \".wppopups-whole\": \"removeIf"
},
{
"path": "inject/inject.js",
"chars": 1079,
"preview": "/* eslint-env browser */\n\n;(function () {\n try {\n const chainToProp = (chain) => {\n return chain\n .split"
},
{
"path": "manifest-v2.json",
"chars": 900,
"preview": "{\n \"name\": \"Demodal - Block modals and overlays\",\n \"description\": \"Demodal automatically removes content blocking moda"
},
{
"path": "manifest-v3.json",
"chars": 849,
"preview": "{\n \"name\": \"Demodal - Block modals and overlays\",\n \"description\": \"Demodal automatically removes content blocking moda"
},
{
"path": "options/options.css",
"chars": 78,
"preview": ".container {\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n}\n"
},
{
"path": "options/options.html",
"chars": 577,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n\n\t\t<title data-i18n=\"options\"></title>\n\n <link rel=\"styles"
},
{
"path": "options/options.js",
"chars": 1042,
"preview": "/* globals chrome, Common */\n\nconst { $, i18n, el, capitalize, modalTypes } = Common\n\ni18n()\n\n//\n;(async () => {\n const"
},
{
"path": "package.json",
"chars": 343,
"preview": "{\n \"devDependencies\": {\n \"@nuxtjs/eslint-config\": \"^3.1.0\",\n \"@nuxtjs/eslint-module\": \"^2.0.0\",\n \"babel-eslint"
},
{
"path": "popup/popup.css",
"chars": 487,
"preview": "body {\n width: 500px;\n}\n\n.header {\n background-color: var(--color-primary);\n display: flex;\n align-items: center;\n "
},
{
"path": "popup/popup.html",
"chars": 4732,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n\n <title></title>\n\n <link rel=\"stylesheet\" href=\"../com"
},
{
"path": "popup/popup.js",
"chars": 5849,
"preview": "/* globals chrome, Common */\n\nconst {\n $,\n $$,\n i18n,\n capitalize,\n removeChildren,\n el,\n debounce,\n modalTypes,"
},
{
"path": "run",
"chars": 939,
"preview": "#!/bin/bash\n\ncase $1 in\n manifest)\n if [[ -f manifest-$2.json ]]; then\n cat manifest-$2.json > manifest.json\n\n "
}
]
About this extraction
This page contains the full source code of the AliasIO/demodal GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (53.5 KB), approximately 14.9k tokens, and a symbol index with 39 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.