Я разработал рекурсивную функцию для захвата «ближайшего» элемента. Это немного отличается от Element.closest
, потому что вместо обхода только непосредственного родителя (ей); он проходит вверх, но проверяет влево-вправо-вверх-вниз.
Есть ли более эффективная проверка пропавших без вести братьев и сестер?
if (Element.prototype.nearest === undefined) {
const nearest = (el, selector) => {
if (el == null) return undefined;
const prev = el.previousElementSibling;
if (prev?.matches(selector)) return prev;
const next = el.nextElementSibling;
if (next?.matches(selector)) return prev;
const parent = el.parentElement;
if (parent?.matches(selector)) return parent;
const relative = parent.querySelector(selector);
if (relative) return relative;
return nearest(el.parentElement, selector);
};
Element.prototype.nearest = function(selector) {
return nearest(this, selector);
};
}
const handleClick = (e) => {
const btn = e.target;
console.log(btn.nearest('.child'));
console.log(btn.nearest('.grand-parent'));
console.log(btn.nearest('#child-1'));
}
document.querySelectorAll('.btn').forEach(btn =>
btn.addEventListener('click', handleClick));
.parent {
display: flex;
}
<div class="grand-parent">
<div class="parent">
<div class="child" id="child-1">
<button class="btn">
Click
</button>
</div>
<div class="child" id="child-2">
<button class="btn">
Click
</button>
</div>
</div>
</div>
1 ответ
Вопрос
«Есть ли более эффективная проверка пропавших без вести братьев и сестер?»
Как это сделано в приведенном вами примере кода. Нет.
Вы можете переписать код, чтобы убрать часть ненужной сложности. Смотрите перезаписи внизу ответа.
Ошибка
Линия…
const relative = parent.querySelector(selector);
… бросит, если parent
является null
Небезопасное расширение прототипа
Без всяких предосторожностей добавлять к существующему прототипу — плохая идея. Element.prototype.nearest
может не использоваться в данный момент, но в любой момент может быть добавлен в DOM, нарушив любой код, в котором вы его используете.
Есть три способа избежать конфликтов / будущих расширений прототипов.
.1 100% безопасность. Не надо !!! расширение прототипа — это открытая ловушка, готовая укусить в любой момент.
.2 Почти безопасно. Используйте нестандартное соглашение об именах, например Element.prototype.select_nearest
Стандарт не будет использовать это имя, но это не означает, что оно на 100% безопасно, так как это имя может использовать кто угодно.
.3 Безопасно, но неудобно. Использовать Символ для определения имени прототипа. Это гарантирует уникальность имени. К сожалению, это означает, что вам нужно будет использовать bracket[notation]
чтобы получить доступ к свойству и поделиться им с кодом, которому требуется доступ. Например
// define
const nearest = Symbol("nearest");
Element.prototype[nearest] = function() { /* ... code ... */ } // prototype is global
// call
btn[nearest](".child");
Чтобы предоставить доступ к символу в глобальной области видимости, используйте Символ. Для
{ // scope block
const nearest = Symbol.for("nearest"); // find or create
Element.prototype[nearest] = function() { log("Nearest") } // prototype is global
}
// Access in unconnected scope
{
Symbol.for("nearest")
btn[nearest](".child");
// or
btn[Symbol.for("nearest")](".child");
}
Стиль
Замечание о стиле.
Плохая привычка не разграничивать блоки кода {}
например if (prev?.matches(selector)) return prev;
безопаснее как if (prev?.matches(selector)) { return prev; }
Несвязанная функция?
Зачем добавлять в прототип элемента функцию, вызывающую несвязанную стрелочную функцию. Это просто делает код чрезмерно сложным.
Вы можете вызвать функцию nearest
непосредственно например nearest(btn, '.child');
или оставьте его на прототипе, избегая вторичного вызова и необходимости передавать this
Смотрите переписывает.
Переписать
Несколько переписываний в качестве примеров, хотя, кроме ошибок и перечисленных выше моментов, я не вижу реальной необходимости в переписывании.
Как прототип
const nearest = Symbol.for("nearest");
Element.prototype[nearest] = function(selector) {
const prev = this.previousElementSibling;
if (prev?.matches(selector)) { return prev }
const next = this.nextElementSibling;
if (next?.matches(selector)) { return next }
const parent = this.parentElement;
if (parent) {
if (parent.matches(selector)) { return parent }
return parent.querySelector(selector) ?? parent[nearest](selector);
}
}
Как функция
function nearest(self, sel) {
const prev = self.previousElementSibling;
if (prev?.matches(sel)) { return prev }
const next = self.nextElementSibling;
if (next?.matches(sel)) { return next }
const parent = self.parentElement;
if (parent) {
if (parent.matches(sel)) { return parent }
return parent.querySelector(sel) ?? parent.nearest(parent, sel);
}
}
или же
const matches = (el, sel) => el && el.matches(sel) ? el : undefined;
function nearest(self, sel) {
const parent = self.parentElement;
const sibling = matches(self.previousElementSibling, sel) ??
matches(self.nextElementSibling, sel) ??
matches(parent, sel) ??
parent?.querySelector(selector) ??
parent?.nearest(parent, selector);
}