CNN TürkMASTERCHEF ELEME ADAYLARI 24 EYLÜL: MasterChef'te dokunulmazlık oyununu kim kazandı?The Jerusalem PostKKL-JNF unveils archival photos showcasing Sukkot celebrations in honor of the holidayPunchEx-PDP, ADC supporters target 100,000 votes for Tinubu, YayiInquirerFarmer with ‘shabu’ caught at Ilocos Sur checkpointESPN Deportes¡En vivo! Tercera práctica en el GP de AzerbaiyánBollywood HungamaLove & War: First look of Alia Bhatt revealed; actress stuns in glamorous cabaret-inspired avatarUOLAnvisa proíbe propaganda da caneta emagrecedora Semavy após anúncios irregularesDaily MailThe Latin American street gang recruiting children in the UK: Feared 'Los Trinitarios' is now linked to three London knife killings... with machetes their weapon of choice to target victimsRTL BoulevardDakota Johnson noemt samenwerking met Taylor Swift 'geweldig'ESPNTransfer value tiers: Which clubs have the most valuable players?Globo EsporteAustrália x Brasil - Amistosos da Seleção Brasileira 2026 - Ao vivo - globoesporte.comPremium TimesCSOs demand disclosure of JBS $2.5bn Nigeria deal, warn of livestock expansion risks
The Daily Newsstand · Free, Always
Friday, September 25, 2026

Удалить что угодно со страницы одним кликом: как устроен «пипеточный» блокировщик на Manifest V3

Translate

Бывает, что блокировщик рекламы не справляется: всплывающее окно подписки, баннер «мы используем cookies», чат-виджет в углу, липкая шапка на пол-экрана. Для этого было маленькое расширение ekill: жмёшь кнопку, наводишь на элемент, кликаешь — элемент исчез. Оно не обновлялось с 2019 года и перестало работать в Chrome, когда Google отключил Manifest V2.

Я переписал его под Manifest V3 для Firefox, Chrome и Edge. Получился SkullClick (GitHub). Задача кажется тривиальной, но у неё неожиданно много граней:

  • сайты, которые перехватывают клики;

  • CSS-селекторы, которые ломаются на современных именах классов;

  • элементы, которые должны исчезать сами при следующем визите.

Разберу по порядку.

Проблема 1: сайт съедает клики

Наивная «пипетка» вешает mousemove и click на document и подсвечивает e.target. На обычных сайтах это работает. На Notion и подобных приложениях — нет: страница сама слушает события на фазе перехвата, вызывает stopPropagation(), и до расширения ничего не доходит. Это issue ekill#35.

Решение в SkullClick — прозрачная полноэкранная подложка внутри shadow root, плюс все обработчики вешаются на window в фазе capture. Это самая ранняя точка, раньше любого обработчика страницы на document или элементах:

for (const type of Object.keys(POINTER_EVENTS)) {
  window.addEventListener(type, onPointerEvent, { capture: true, passive: false });
}

function onPointerEvent(e) {
  if (!e.composedPath().includes(ui.overlay)) return; // событие не по нашей подложке
  POINTER_EVENTS[e.type](e);                           // mousedown/up, pointer*, dblclick... — проглатываем
}

Но если всё закрыто подложкой, как понять, что под курсором? Для этого есть document.elementsFromPoint(x, y). Он возвращает все элементы в точке, сверху вниз. Мы пропускаем свои и берём первый элемент страницы:

function elementAt(x, y) {
  for (const el of document.elementsFromPoint(x, y)) {
    if (isOurs(el)) continue;
    if (el === document.documentElement || el === document.body) return null;
    return el;
  }
  return null;
}

Побочный плюс: так же подсвечиваются iframe. Старый подход требовал отдельной подложки для каждого фрейма, здесь это не нужно. А DOM и классы страницы во время выбора не трогаются вообще.

Минус подложки в том, что она перехватывает и колесо мыши. Основная страница прокручивается сама, а для вложенных прокручиваемых блоков (модалки со своим скроллом) пришлось вручную найти ближайшего прокручиваемого предка и вызвать scrollBy.

Проблема 2: выбрать не то

Кликаешь по окну подписки, а удаляется только кнопка внутри него. Или удаляется диалог, но тёмный полупрозрачный фон остаётся. Поэтому в SkullClick:

  • ↑ расширяет выделение до родителя, ↓ возвращает к ребёнку (стек запоминает путь);

  • Shift+клик удаляет и оставляет пипетку включённой;

  • Ctrl+Z — отмена.

Скрывать, а не удалять

ekill делал element.remove(). Отказаться от этого пришлось по двум причинам:

  1. Отмену так не сделать. Удалённый узел можно вставить обратно, но фреймворк страницы к этому времени может перерисовать соседей.

  2. Селекторы разъезжаются. Запомненные правила используют позицию :nth-child. Удалишь один элемент — индексы соседей сдвинутся, и правило для следующего элемента начнёт бить мимо (ekill#22).

Поэтому элемент просто скрывается через display: none !important, а прежнее значение display запоминается для отмены. Скрытое видео при этом продолжает играть, так что у видео и аудио вызывается pause(), а iframe (встроенные плееры, чаты) получает src="about:blank". При отмене всё восстанавливается.

Проблема 3: селекторы для «злопамятного» режима

Необязательный режим Grudge («злопамятный») запоминает удалённые элементы и сам скрывает их при следующих визитах на эту страницу или весь сайт. Для этого нужен CSS-селектор, однозначно указывающий на элемент.

В ekill селектор собирался из #id и .class склейкой строк. На современной вёрстке это ломается. Классы вида u-hidden@desktop-max (BEM с модификаторами), ver-5.7, md:flex из Tailwind или id, начинающийся с цифры, дают невалидный селектор, и querySelector бросает исключение (ekill#30, #34). Лечится через CSS.escape():

CSS.escape("u-hidden@desktop-max") // "u-hidden\\@desktop-max"
CSS.escape("5col")                  // "\\35 col"

Генератор идёт от элемента вверх и на каждом шаге проверяет через querySelectorAll, что селектор уже однозначен. Если нет, добавляет следующего предка или :nth-child.

Как применять правила: insertCSS, а не JavaScript

Скрыть запомненные элементы можно двумя способами:

  • JavaScript. Найти элементы и поставить display: none. Но элемент может появиться позже (модалки часто всплывают через 5 секунд), и тогда нужен MutationObserver или опрос.

  • CSS-правило. Оно действует и на элементы, которые появятся позже, без всякого наблюдения. Но <style> на странице с жёсткой CSP заблокируют.

Выход — chrome.scripting.insertCSS. Стили, вставленные расширением, под CSP страницы не попадают:

// Одно правило на селектор: один кривой селектор не отключит остальные
const css = selectors.filter(isSafeSelector).map((s) => `${s} { display: none !important; }`).join("\n");
await chrome.scripting.insertCSS({ target: { tabId, frameIds: [frameId] }, css });

Здесь две защиты. Каждое правило отдельное: если один селектор окажется невалидным, браузер отбросит только его. А isSafeSelector не пропускает селекторы с {, } и /*. Список правил можно импортировать из файла, и без этой проверки из такого файла можно было бы вставить на страницу произвольный CSS.

Для одностраничных приложений есть ещё один нюанс: при навигации без перезагрузки меняется путь, и правила прошлой страницы нужно снять через removeCSS, а для новой — вставить.

Меньше прав, чем у оригинала

ekill внедрял скрипт в каждую страницу, то есть требовал доступ ко всем сайтам всегда. В SkullClick по умолчанию только activeTab и scripting: скрипт внедряется в текущую вкладку в момент нажатия кнопки. Доступ ко всем сайтам (optional_host_permissions) запрашивается только если пользователь включит режим Grudge. Тогда контент-скрипт регистрируется динамически:

if (settings.grudge && (await chrome.permissions.contains({ origins: ["<all_urls>"] }))) {
  await chrome.scripting.registerContentScripts([
    { id: "skullclick-grudge", matches: ["<all_urls>"], js: CONTENT_FILES, runAt: "document_start" },
  ]);
}

Выключили режим — скрипт снимается. Для расширения, которое работает на любой странице, это существенная разница в доверии.

Тестирование

Юнит-тесты на jsdom покрывают генератор селекторов и правила, включая реальные случаи из issues ekill: классы u-hidden@desktop-max и js-comp-ver-5.7.

E2E на Puppeteer гоняются в Chrome, Firefox и Edge на локальных тестовых страницах:

  • обычная страница с модалкой;

  • страница, которая глушит все события мыши через stopPropagation();

  • чат-виджет в iframe.

Проверяются удаление, отмена, выбор родителя, запоминание и автоматическое скрытие при следующем визите.

Итог

  • Кликнул — элемент исчез. Работает на всплывающих окнах, баннерах, липких шапках, чатах, iframe.

  • Отмена, выбор родителя и ребёнка стрелками, удаление нескольких элементов подряд.

  • Необязательный режим «помнить и скрывать всегда», список правил с экспортом и импортом.

  • Работает на сайтах, которые перехватывают клики, и со строгой CSP.

  • Firefox, Chrome, Edge. Без сети и аналитики, MIT (как у оригинала).

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.