Уроки рисования красных квадратов
Всем привет. На этот раз покажу как парсил dom элементы и создавал для них визуальную подсветку. Разберу зачем это нужно и как сделано. Спойлер, очень просто.
Значительную часть статьи рассказываю на чём я споткнулся, пока это писал. Про высчитывание позиции блока.
Продолжаю эпопею. Вот вводная статья: Интерактивный парсер web страниц / Хабр (habr.com)
Менеджер страницы (PageManager)
В какой-то момент я понял, что создавать addEventListener и давать ему бесконтрольно запускать код — ужасная затея. Расчёты сложные и их много.
Приложение может уйти в бесконечную рекурсию, а если эвенты при этом продолжают создаваться, то можно не рассчитывать на то что зависшую вкладку будет легко закрыть.
Поэтому я принял решение отслеживать activeBlock и продолжать обрабатывать event только при смене блока.
if (!this.sameBlock(currentBlock, this.#activeBlock)) {
...
}
Собственно в этом и состоит задача PageManager’а. Следить за состоянием страницы. Он же и будет производить основные манипуляции со страницей. Например рисовать подсветку блоков?
const highlighting = new BlockHighlighting(this.#activeBlock);
highlighting.draw();
Объясню. Фактически подсветка будет нужна не блоку, а словам. Причём не всем, а только лишь тем, которые уже были добавлены в словарь. Но почему я подсвечиваю блок?
Для отладки :)
Ранее я уже показывал блок с параметрами.
И если честно, в один момент я начал в них путаться и теряться. Тогда стало понятно — надо делать нагляднее. Всё равно эти блоки рисовать придётся, так почему бы не сделать это сейчас?
Итого мы имеем такой вот класс:
export class PageManager {
// element который у нас сейчас нарисован
#activeBlock;
run = () => {
//Непосредственно тут мы начинаем обрабатывать страницу
addEventListener("mousemove", this.#onmousemove);
}
#onmousemove = (event) => {
// Создаём объект с информацией о размерах блока и ссылкой на dom element
const currentBlock = this.#whereWeAre(event);
// Проверяем что мы не парсим одно и то же, сжирая ресурсы
if (!this.#sameBlock(currentBlock, this.#activeBlock)) {
// Раз мы теперь в другом блоке, надо перезаписать активный блок
this.#activeBlock = currentBlock;
// Далее мы рисуем красный квадрат
const highlighting = new BlockHighlighting(this.#activeBlock);
highlighting.draw();
}
}
#sameBlock = (currentFocusBlock, activeBlock) => {
// Провряем что это у нас не первый запуск + сама проверка на "sameBlock"
return (currentFocusBlock && activeBlock) && currentFocusBlock.getRef() === activeBlock.getRef();
}
#whereWeAre = (event) => {
// FocusBlock разберу дальше.
return new FocusBlock(event);
}
}
Как видно на картинке выше, блок может поменяться в размерах когда он уже взят в фокус. Поэтому правильным решением будет ловить эвентики пользователя и в целом страницы. Про это как ни будь в других статьях.
Определение размеров и позиций блоков
Казалось бы. Тривиальная тема, всё очевидно. Бери offsetLeft, offsetTop у элемента и будет тебе счастье, но нет.
На некоторых страницах это решение работает. Но не везде. Вот например корейский naver:
const x = this.#ref.offsetLeft;
const y = this.#ref.offsetTop;
this.#position = {x, y}
Сначала я предположил — мы теряем offset родителей. И оказался прав. Частично.
Добавлю следующий код, который пройдётся по родительским элементам и получу:
let parent = this.#ref.offsetParent;
while (parent && parent.offsetParent) {
parent = parent.offsetParent;
this.#position.x += parent.offsetLeft;
this.#position.y += parent.offsetTop;
}
Ммм… Почти идеально, но что-то не так. Есть ещё какие-то отступы которые мы потеряли.
Пробую выгрузить padding, margin… Вижу этого блока нули и пиксели.
padding-top: 0 px
margin-top: 0 px
Понимаю что если вместо пикселей будет vw, wh или проценты, то будет так себе. Решение писать регулярки и парсить родителей элемента отметаю. Иду искать стандартное решение.
И оно есть! getBoundingClientRect:
const x = this.#rect.x + window.scrollX;
const y = this.#rect.y + window.scrollY;
this.#position = {x, y}
Эту статью я уже пишу с подсветочкой :)
И на naver работает:
А вот собственно полный код класса, который занимается выгрузкой размеров и позиций элементов:
export class FocusBlock {
#ref;
#size;
#position;
#rect;
constructor(event) {
this.#ref = event.target;
this.#rect = this.#ref.getBoundingClientRect();
this.#setupSize();
this.#setupPosition();
}
#setupPosition = () => {
const x = this.#rect.x + window.scrollX;
const y = this.#rect.y + window.scrollY;
this.#position = {x, y}
}
#setupSize = () => {
const width = this.#rect.width;
const height = this.#rect.height;
this.#size = {width, height}
}
getRef = () => {
return this.#ref;
}
getSize = () => {
return this.#size;
}
getPosition = () => {
return this.#position;
}
Отрисовка подсветки (BlockHighlighting)
Ну и на добивочку. Отрисовка блока. Разберу вкратце пару моментов, тк тут всё просто.
Во первых я записываю ссылку на dom element в статическую переменную static draw;
Статика нужна для того, чтобы элемент на странице точно был один. Наплодить миллион лишних div элементов — очень так себе идея.
Тем не менее, сейчас я понимаю, что статику убрать придётся, когда мне потребуется рисовать подсветку для слов. Тема мне на подумать. Видимо придётся делать некий деструктор и управлять им в PageManager.
Следующий:
if (BlockHighlighting.draw) {
window.document.body.removeChild(BlockHighlighting.draw);
}
Удаляем draw каждый раз при перерендере блока. То бишь, когда блок меняется.
В конструктор мы передавали block. А вот зачем:
BlockHighlighting.draw.style.left = `${this.#block.getPosition().x}px`;
BlockHighlighting.draw.style.top = `${this.#block.getPosition().y}px`;
BlockHighlighting.draw.style.width = `${this.#block.getSize().width}px`;
BlockHighlighting.draw.style.height = `${this.#block.getSize().height}px`;
Всё это дело мы посчитали на прошлом шаге, тут нам всего лишь остаётся заполнить наш новенький div. Если хоть один из этих параметров будет не заполнен, то элемента на странице мы не увидим. Тестировал на naver.
И наконец:
BlockHighlighting.draw.style.pointerEvents = "none";
Мы же не хотим, чтобы наш EventListener словил бэд трип от этого псевдо элемента? Так вот, мы его выключаем. Парсить draw — не будем. Ну и попутно он не будет мешать пользователю, преграждая ему путь до заветных ссылочек и кнопочек.
Вот полный код класса:
export class BlockHighlighting {
static draw;
#block;
constructor(block) {
this.#block = block;
}
draw = () => {
if (BlockHighlighting.draw) {
window.document.body.removeChild(BlockHighlighting.draw);
}
BlockHighlighting.draw = window.document.createElement("div");
BlockHighlighting.draw.style.left = `${this.#block.getPosition().x}px`;
BlockHighlighting.draw.style.top = `${this.#block.getPosition().y}px`;
BlockHighlighting.draw.style.width = `${this.#block.getSize().width}px`;
BlockHighlighting.draw.style.height = `${this.#block.getSize().height}px`;
BlockHighlighting.draw.style.position = "absolute";
BlockHighlighting.draw.style.background = "red";
BlockHighlighting.draw.style.zIndex = "1000";
BlockHighlighting.draw.style.opacity = "0.25";
BlockHighlighting.draw.style.pointerEvents = "none";
window.document.body.appendChild(BlockHighlighting.draw);
}
}
Я обещал статью про парсинг слов в тексте. Считайте что это была подводочка. Сам код парсера слов пока что очень грязный и такой парсинг блоков поможет мне с оптимизацией и более точным поиском слова в фокусе, поэтому начать нужно было отсюда.
На этом у меня всё. В будущем тут появится ссылочка на следующую статью.