Продолжаем упрощать жизнь интернет-пользователей с помощью браузерных расширений. Напомним, что мы уже рассказывали, как создать:
Сегодня же мы создадим расширение, убирающее мусор.
Что есть мусор? Здесь каждый решает сам. Формально речь о любом элементе сайта. Однако, по задумке, это разного рода реклама/попапы и прочий «мусор», который перекрывает обзор и мешает пользоваться сайтом. Хотя ничто не мешает удалять любые другие элементы. И даже «родительские элементы» этих элементов (что полезно, если на попапе много мелких деталей).
Немного важной инфы
Заранее информируем, что в статье опущены базовые принципы работы расширений. Если вы еще не занимались подобным — вам сюда. Там в деталях разобрано, что, как и почему. Если же вы уже имеете опыт создания расширений — то это вам ни к чему. Само расширение будет состоять из:
manifest.json — обязательный файл, благодаря которому браузер в принципе понимает, что это — расширение.
content.js — файл с основной логикой работы.
background.js — файл с логикой работы контекстного меню. Увы, специфика MV3 не позволяет взаимодействовать с контекстным меню из content.js, а «переделывать» его — это автоматически получить проблемы совместимости с другими расширениями, изменяющими его. Поэтому — background.js.
popup.html — менюшка управления включением/выключением расширения на сайтах.
popup.js — логика описанной выше менюшки.
Создаем расширение, удаляющее мусор с сайтов
Итак, начнем. Для начала создаем папку с любым названием (лучше без кириллицы), а в ней создаем 5 ранее упомянутых файлов — manifest.json, content.js, background.js, popup.html, popup.js. Затем делаем следующее:
1. Открываем manifest.json и копируем в него:
{
"manifest_version":3,
"name":"Element Cleaner Per Site",
"version":"2.0.0",
"description":"Удаление элементов страниц через контекстное меню с включением по отдельным сайтам.",
"permissions":[
"contextMenus",
"tabs",
"storage",
"activeTab"
],
"host_permissions":[
"<all_urls>"
],
"background":{
"service_worker":"background.js"
},
"action":{
"default_title":"Element Cleaner",
"default_popup":"popup.html"
},
"content_scripts":[
{
"matches":[
"<all_urls>"
],
"js":[
"content.js"
],
"run_at":"document_start"
}
]
}
2. Сохраняем manifest.json
3. Открываем content.js и копируем в него:
let selectedElement=null;
let hoverElement=null;
let extensionEnabled=false;
let lastOutline="";
function checkState(){
chrome.storage.sync.get(
["enabledSites"],
result=>{
const sites=
Array.isArray(result.enabledSites)
? result.enabledSites
: [];
extensionEnabled=
sites.includes(location.hostname);
if(!extensionEnabled){
removeHighlight();
}
}
);
}
function applyHighlight(element){
if(!element) return;
if(hoverElement===element){
return;
}
removeHighlight();
hoverElement=element;
lastOutline=
element.style.outline;
element.style.outline=
"3px solid red";
element.style.outlineOffset=
"2px";
}
function removeHighlight(){
if(hoverElement){
hoverElement.style.outline=
lastOutline || "";
hoverElement.style.outlineOffset="";
}
hoverElement=null;
lastOutline="";
}
document.addEventListener(
"mousemove",
event=>{
if(!extensionEnabled){
return;
}
const element=
event.target;
if(
element &&
element!==document.body &&
element!==document.documentElement
){
applyHighlight(element);
}
},
true
);
document.addEventListener(
"mouseleave",
()=>{
removeHighlight();
},
true
);
document.addEventListener(
"contextmenu",
event=>{
if(!extensionEnabled){
return;
}
selectedElement=
event.target;
applyHighlight(selectedElement);
},
true
);
chrome.storage.onChanged.addListener(()=>{
checkState();
});
chrome.runtime.onMessage.addListener(
message=>{
if(message.action==="remove_element"){
if(
extensionEnabled &&
selectedElement
){
selectedElement.remove();
selectedElement=null;
}
}
if(message.action==="remove_parent_element"){
if(
extensionEnabled &&
selectedElement
){
const parent=
selectedElement.parentElement;
if(parent){
parent.remove();
}
selectedElement=null;
}
}
}
);
checkState();
4. Сохраняем content.js
5. Открываем background.js и копируем в него:
const REMOVE_MENU_ID="remove_element";
const REMOVE_PARENT_MENU_ID="remove_parent_element";
function getHostname(url){
try{
return new URL(url).hostname;
}catch(e){
return "";
}
}
function getEnabledSites(){
return new Promise(resolve=>{
chrome.storage.sync.get(
["enabledSites"],
result=>{
resolve(
Array.isArray(result.enabledSites)
? result.enabledSites
: []
);
}
);
});
}
async function isEnabledForTab(tab){
if(!tab || !tab.url){
return false;
}
const hostname=getHostname(tab.url);
const sites=await getEnabledSites();
return sites.includes(hostname);
}
function createContextMenu(){
chrome.contextMenus.removeAll(()=>{
chrome.contextMenus.create({
id:REMOVE_MENU_ID,
title:"Удалить элемент",
contexts:["all"]
});
chrome.contextMenus.create({
id:REMOVE_PARENT_MENU_ID,
title:"Удалить родительский элемент",
contexts:["all"]
});
});
}
async function refreshMenu(tabId,url){
const enabled=
(await getEnabledSites())
.includes(getHostname(url));
if(enabled){
createContextMenu();
}else{
chrome.contextMenus.removeAll();
}
}
chrome.runtime.onInstalled.addListener(()=>{
chrome.contextMenus.removeAll();
});
chrome.storage.onChanged.addListener(()=>{
chrome.tabs.query(
{},
tabs=>{
tabs.forEach(tab=>{
if(tab.url){
refreshMenu(
tab.id,
tab.url
);
}
});
}
);
});
chrome.tabs.onActivated.addListener(info=>{
chrome.tabs.get(
info.tabId,
tab=>{
if(tab && tab.url){
refreshMenu(
tab.id,
tab.url
);
}
}
);
});
chrome.tabs.onUpdated.addListener(
(tabId,changeInfo,tab)=>{
if(changeInfo.url){
refreshMenu(
tabId,
changeInfo.url
);
}
}
);
chrome.contextMenus.onClicked.addListener(
async(info,tab)=>{
if(!await isEnabledForTab(tab)){
return;
}
chrome.tabs.sendMessage(
tab.id,
{
action:info.menuItemId
}
);
}
);
6. Сохраняем background.js
7. Открываем popup.html и копируем в него:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Element Cleaner</title>
<style>
body{
width:350px;
padding:14px;
margin:0;
font-family:Arial,sans-serif;
background:#f7f7f7;
color:#222;
}
h2{
margin:0 0 12px 0;
font-size:20px;
}
.description{
font-size:12px;
opacity:.75;
line-height:1.4;
margin-bottom:12px;
}
.panel{
background:white;
border:1px solid #d5d5d5;
padding:10px;
margin-bottom:12px;
}
.caption{
font-size:12px;
opacity:.7;
}
.current-site{
margin-top:6px;
font-weight:bold;
word-break:break-all;
}
.status{
margin-top:8px;
padding:6px;
text-align:center;
border:1px solid #ddd;
font-size:12px;
}
button{
width:100%;
padding:8px;
margin-top:8px;
cursor:pointer;
}
.sites-title{
margin-bottom:8px;
}
.sites-list{
max-height:170px;
overflow-y:auto;
border:1px solid #ddd;
background:#fff;
}
.site-row{
display:flex;
justify-content:space-between;
align-items:center;
padding:6px;
border-bottom:1px solid #eee;
font-size:12px;
}
.site-name{
overflow:hidden;
text-overflow:ellipsis;
}
.delete-button{
width:auto;
margin:0;
padding:3px 8px;
}
.empty{
padding:8px;
opacity:.6;
font-size:12px;
}
.footer{
margin-top:10px;
text-align:center;
font-size:11px;
opacity:.5;
}
</style>
</head>
<body>
<h2>
Element Cleaner
</h2>
<div class="description">
Расширение полностью включается только на выбранных сайтах.
На остальных страницах оно не добавляет пункты в контекстное меню.
</div>
<div class="panel">
<div class="caption">
Текущий сайт:
</div>
<div
id="currentSite"
class="current-site"
>
Определение...
</div>
<div
id="status"
class="status"
>
Проверка состояния...
</div>
<button
id="toggleButton"
>
Загрузка...
</button>
</div>
<div class="panel">
<div class="sites-title">
Активные сайты:
</div>
<div
id="sitesList"
class="sites-list"
>
</div>
<button
id="addButton"
>
Добавить текущий сайт
</button>
</div>
<div class="footer">
Element Cleaner • Site control
</div>
<script src="popup.js"></script>
</body>
</html>
8. Сохраняем popup.html
9. Открываем popup.js и копируем в него:
const currentSiteElement =
document.getElementById("currentSite");
const statusElement =
document.getElementById("status");
const toggleButton =
document.getElementById("toggleButton");
const addButton =
document.getElementById("addButton");
const sitesListElement =
document.getElementById("sitesList");
let currentHostname="";
let enabledSites=[];
function getCurrentTab(){
return new Promise(resolve=>{
chrome.tabs.query(
{
active:true,
currentWindow:true
},
tabs=>{
if(tabs && tabs[0]){
resolve(tabs[0]);
}else{
resolve(null);
}
}
);
});
}
function extractHostname(url){
try{
return new URL(url).hostname;
}catch(error){
return "";
}
}
function loadEnabledSites(){
return new Promise(resolve=>{
chrome.storage.sync.get(
["enabledSites"],
result=>{
if(
result &&
Array.isArray(result.enabledSites)
){
resolve(
result.enabledSites
);
}else{
resolve([]);
}
}
);
});
}
function saveEnabledSites(list){
return new Promise(resolve=>{
chrome.storage.sync.set(
{
enabledSites:list
},
()=>{
resolve();
}
);
});
}
function isEnabled(site){
return enabledSites.includes(site);
}
function updateStatus(){
if(isEnabled(currentHostname)){
statusElement.textContent=
"Расширение активно на этом сайте";
toggleButton.textContent=
"Выключить расширение на сайте";
}else{
statusElement.textContent=
"Расширение выключено на этом сайте";
toggleButton.textContent=
"Включить расширение на сайте";
}
}
function renderCurrentSite(){
currentSiteElement.textContent=
currentHostname || "Не найден";
updateStatus();
}
function renderSites(){
sitesListElement.innerHTML="";
if(enabledSites.length===0){
const empty=
document.createElement("div");
empty.className="empty";
empty.textContent=
"Нет активных сайтов";
sitesListElement.appendChild(empty);
return;
}
enabledSites.forEach(site=>{
const row=
document.createElement("div");
row.className=
"site-row";
const name=
document.createElement("div");
name.className=
"site-name";
name.textContent=
site;
const button=
document.createElement("button");
button.className=
"delete-button";
button.textContent=
"Удалить";
button.onclick=
async()=>{
enabledSites=
enabledSites.filter(
item=>
item!==site
);
await saveEnabledSites(
enabledSites
);
renderSites();
renderCurrentSite();
};
row.appendChild(name);
row.appendChild(button);
sitesListElement.appendChild(row);
});
}
async function toggleSite(){
if(!currentHostname){
return;
}
if(isEnabled(currentHostname)){
enabledSites=
enabledSites.filter(
item=>
item!==currentHostname
);
}else{
enabledSites.push(
currentHostname
);
}
await saveEnabledSites(
enabledSites
);
renderSites();
renderCurrentSite();
}
async function addCurrentSite(){
if(!currentHostname){
return;
}
if(!isEnabled(currentHostname)){
enabledSites.push(
currentHostname
);
await saveEnabledSites(
enabledSites
);
}
renderSites();
renderCurrentSite();
}
function bindEvents(){
toggleButton.onclick=
toggleSite;
addButton.onclick=
addCurrentSite;
}
async function init(){
const tab=
await getCurrentTab();
if(!tab){
return;
}
currentHostname=
extractHostname(tab.url);
enabledSites=
await loadEnabledSites();
renderCurrentSite();
renderSites();
bindEvents();
}
document.addEventListener(
"DOMContentLoaded",
init
);
10. Сохраняем popup.js
Затем открываем Chrome и жмем на «три точки», затем на «Расширения», далее на «Управления расширениями» и на «Загрузить распакованное расширение».
Находим нашу папку и выбираем ее. Тестируем, все ли работает, не забыв предварительно включить расширение для конкретного сайта.
Саммари
Как видите, удалять с сайта элементы, которые вам мешают, не так уж и сложно — достаточно уделить пару минут и сделать для этого специальное разрешение. И если в случае с безобидным попапом слева снизу это не столь актуально, то в случае с, скажем, огромным окном на весь экран, мешающим пользоваться сайтом, начинаешь понимать, насколько это удобно. Тем более что собственное приложение не имеет сигнатур AdBlock и не палится, что, в свою очередь, позволяет заходить на сайты, которые блокируют подключение при обнаружении AdBlock.