Удаление раздражающих элементов на страницах — всплывающих баннеров, плашек с куки или липких шапок — кажется простой задачей. Однако с переходом на платформу Chrome Manifest V3 и усложнением современных интерфейсов (Notion, Jira, Miro) прежние подходы перестали работать. Популярная утилита ekill 2019 года устарела, а ее прямое портирование наткнулось на защитные ограничения браузеров.
Разработчик Владимир (@Perruer) переписал инструмент под Manifest V3, выпустив расширение с открытым исходным кодом SkullClick (лицензия MIT). В проекте решен комплекс задач: от перехвата событий мыши в обход изоляции до надежной генерации CSS-селекторов.
Перехват кликов и поиск элементов сквозь оверлей
Главная проблема расширений-«пипеток» — перехват кликов. Сервисы вроде Notion вешают обработчики на уровне document и вызывают e.stopPropagation(). Расширение, слушающее события на фазе всплытия (bubble), кликов попросту не получает.
SkullClick решает задачу через оверлей и раннюю фазу погружения (capture phase):
- Интерфейс подсветки изолируется в Shadow DOM, чтобы стили страницы не ломали разметку инструмента.
- Обработчики событий мыши регистрируются на объекте
windowс флагом{ capture: true, passive: false }. - Поскольку прозрачная подложка перекрывает экран, целевой узел под курсором находится методом
document.elementsFromPoint(x, y)с фильтрацией элементов самого расширения.
// Перехват событий на ранней фазе capture
const POINTER_EVENTS = ['mousedown', 'mouseup', 'click', 'pointerdown'];
for (const type of POINTER_EVENTS) {
window.addEventListener(type, onPointerEvent, { capture: true, passive: false });
}
function onPointerEvent(e) {
if (!e.composedPath().includes(uiOverlayElement)) return;
e.preventDefault();
e.stopPropagation();
const target = getElementUnderCursor(e.clientX, e.clientY);
if (target && e.type === 'click') {
hideElement(target);
}
}
function getElementUnderCursor(x, y) {
const elements = document.elementsFromPoint(x, y);
for (const el of elements) {
if (isExtensionElement(el)) continue;
if (el === document.documentElement || el === document.body) return null;
return el;
}
return null;
}
Почему нельзя вызывать remove()
В ранних утилитах элемент удалялся вызовом element.remove(). В SPA-приложениях это создает две проблемы:
- Физическое удаление узла нарушает отмену действия (
Ctrl+Z), если приложение перерисовало состояние. - Удаление узла сдвигает индексы
:nth-child(n)у соседей, из-за чего персистентные правила бьют по чужим блокам.
В SkullClick элемент скрывается правилом display: none !important. Для медиаплееров вызывается .pause(), а для встроенных фреймов очищается адрес (iframe.src = "about:blank"), исключая фоновое воспроизведение звука.
Генерация селекторов с экранированием
Для долговременного сокрытия расширение сохраняет CSS-селектор. В современных проектах классы Tailwind CSS (md:flex, hover:bg-red-500) и БЭМ-модификаторы (u-hidden@desktop-max) содержат спецсимволы, вызывающие синтаксические ошибки в document.querySelector().
SkullClick экранирует каждый класс методом CSS.escape():
// Генерация однозначного CSS-селектора с экранированием классов
function buildSafeSelector(el) {
if (el.id && !/^\d/.test(el.id)) {
return `#${CSS.escape(el.id)}`;
}
const parts = [];
let current = el;
while (current && current !== document.body) {
let selector = current.tagName.toLowerCase();
if (current.classList.length > 0) {
const classes = Array.from(current.classList)
.map((cls) => `.${CSS.escape(cls)}`)
.join('');
selector += classes;
}
if (document.querySelectorAll(selector).length === 1) {
parts.unshift(selector);
return parts.join(' > ');
}
const parent = current.parentElement;
if (parent) {
const index = Array.from(parent.children).indexOf(current) + 1;
selector += `:nth-child(${index})`;
}
parts.unshift(selector);
current = parent;
}
return parts.join(' > ');
}
Стили внедряются через chrome.scripting.insertCSS, не блокируясь строгими политиками Content Security Policy (CSP).
Установка и горячие клавиши
Расширение доступно в репозитории https://github.com/Perruer/skullclick. В Chrome и Edge его можно подключить через chrome://extensions, включив «Режим разработчика» и выбрав распакованный архив.
Пипетка вызывается сочетанием Alt+Shift+K или иконкой на панели. Клавиши ↑ и ↓ расширяют или сужают область выбора в дереве DOM, Shift+клик запускает серийное скрытие, а Ctrl+Z отменяет последнее действие.
