Сравнение элементов HTML по фактическому z-индексу

Учитывая два абстрагированных HTML-элемента A и B в том же документе, как я могу узнать, какой из них "ближе" к пользователю (т.е. если они перекрываются, какой из них скрывает другой)?

Спецификация CSS W3C описывает контексты стекирования, которые должны реализовывать совместимые механизмы визуализации. Однако я не мог найти способ доступа к этой информации в программе JavaScript, кросс-браузерах или нет. Все, что я могу прочитать, это свойство css z-index, которое само по себе не говорит много, так как большая часть времени установлена ​​на auto или даже когда выражается как числовое значение, не является надежным индикатором того, как это (если они принадлежат к различным контекстам статистики, сравнение z-индексов не имеет значения).

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

Обновление: Позвольте мне немного пояснить причину этого вопроса: недавно я рассмотрел вопрос , который выявил ограничение в jQuery drag и drop - во время сброса он не учитывает z-индексы, поэтому, если элемент скрывает другой, он все равно может выполнить операцию drop в элементе, который находится "позади". Хотя на связанный вопрос был дан ответ на конкретный случай OP, общая проблема сохраняется, и нет простого решения, о котором я знаю.

alex answer ниже полезно, но недостаточно для рассматриваемого случая: при перетаскивании сам перетаскиваемый элемент (или, точнее, его помощник) является самым верхним элементом под курсором мыши, поэтому elementFromPoint вернет it вместо next верхнего элемента, который нам действительно нужен (обходной путь: стиль курсора, чтобы он помещался вне помощника). Другие стратегии пересечения, используемые jQuery, также учитывают больше, чем одну точку, что усложняет задачу определения самого верхнего элемента, который каким-то образом пересекает помощника. Возможность сравнить (или отсортировать) элементы по фактическому z-индексу сделает режим пересечения "z-index aware" жизнеспособным для общего случая.

Ответ 1

После нескольких дней исследований, я думаю, что я успешно переработал механизм стекирования в соответствии с правилами 2016 года. Я в основном обновил подход 2013 года (опубликовано OP). Результатом является функция, которая сравнивает два узла DOM и возвращает визуально сверху.

front = $.fn.visuallyInFront(document.documentElement, document.body);
// front == <body>...</body> because the BODY node is 'on top' of the HTML node

Рассуждение

Существуют другие способы определить, какой элемент находится поверх другого. Например, document.elementFromPoint() или document.elementsFromPoint() spring. Однако есть много (недокументированных) факторов, которые влияют на надежность этих методов. Например, непрозрачность, видимость, указатели-события, обратная видимость и некоторые преобразования могут сделать document.elementFromPoint() неспособным выполнить тест определенного элемента. И тогда возникает проблема, что document.elementFromPoint() может запрашивать только самый верхний элемент (не лежащий в основе). Это должно быть разрешено с помощью document.elementsFromPoint(), но в настоящее время оно реализовано только в Chrome. Кроме того, я написал ошибку с разработчиками Chrome о document.elementsFromPoint(). При тестировании тега привязки все базовые элементы остаются незамеченными.

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

Как это работает

Мой подход повторно реализует механизм стекирования HTML. Он направлен на правильное следование всем правилам, которые влияют на порядок укладки элементов HTML. Это включает в себя правила позиционирования, поплавки, порядок DOM, а также свойства CSS3, такие как прозрачность, трансформирование и другие экспериментальные свойства, такие как фильтр и маска. Правила, похоже, правильно реализованы с марта 2016 года, но их необходимо будет обновить в будущем при изменении спецификации и поддержки браузера.

Я собрал все вместе в репозиторий GitHub. Надеемся, что этот подход будет продолжать работать надежно. Вот пример пример JSFiddle кода в действии. В примере все элементы сортируются по фактическому "z-index", что и было после OP.

Тестирование и обратная связь по этому подходу были бы очень желанными!

Ответ 2

Примечание: после более чем одного года без ответа этот вопрос был также опубликован в Qaru на португальском языке и - пока все еще без окончательное решение - некоторые пользователи и я смогли реплицировать механизм стекирования в JavaScript (изобретать колесо, но все же...)

Указание алгоритма контекста стекирования в спецификация CSS2 (выделено мной):

Корневой элемент формирует контекст укладки корня. Другие контексты стеков генерируются любым элементом позицией (включая относительно позиционированные элементы) с вычисленным значением "z-index", кроме "auto" . Контексты стеков не обязательно связаны с блоками. В будущих уровнях CSS другие свойства могут вводить контексты стекирования, , например, "непрозрачность"

Из этого описания здесь возвращает функцию: a) z-index элемента, если она генерирует новый контекс стекирования; или b) undefined, если это не означает <

function zIndex(ctx) {
    if ( !ctx || ctx === document.body ) return;

    var positioned = css(ctx, 'position') !== 'static';
    var hasComputedZIndex = css(ctx, 'z-index') !== 'auto';
    var notOpaque = +css(ctx, 'opacity') < 1;

    if(positioned && hasComputedZIndex) // Ignoring CSS3 for now
        return +css(ctx, 'z-index');
}

function css(el, prop) {
     return window.getComputedStyle(el).getPropertyValue(prop);
}

Это должно быть способно выделять элементы, которые формируют различные контексты стекирования. Для остальных элементов (и для элементов с равным z-index) Приложение E говорит, что они должны уважать "порядок дерева":

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

За исключением тех свойств, которые перемещают прямоугольники вокруг, эта функция shoud правильно реализует обход:

/* a and b are the two elements we want to compare.
 * ctxA and ctxB are the first noncommon ancestor they have (if any)
 */
function relativePosition(ctxA, ctxB, a, b) {
    // If one is descendant from the other, the parent is behind (preorder)
    if ( $.inArray(b, $(a).parents()) >= 0 )
        return a;
    if ( $.inArray(a, $(b).parents()) >= 0 )
        return b;
    // If two contexts are siblings, the one declared first - and all its
    // descendants (depth first) - is behind
    return ($(ctxA).index() - $(ctxB).index() > 0 ? a : b);
}

С помощью этих двух функций мы можем, наконец, создать нашу функцию сравнения элементов:

function inFront(a, b) {
    // Skip all common ancestors, since no matter its stacking context,
    // it affects a and b likewise
    var pa = $(a).parents(), ia = pa.length;
    var pb = $(b).parents(), ib = pb.length;
    while ( ia >= 0 && ib >= 0 && pa[--ia] == pb[--ib] ) { }

    // Here we have the first noncommon ancestor of a and b  
    var ctxA = (ia >= 0 ? pa[ia] : a), za = zIndex(ctxA);
    var ctxB = (ib >= 0 ? pb[ib] : b), zb = zIndex(ctxB);

    // Finds the relative position between them    
    // (this value will only be used if neither has an explicit
    // and different z-index)
    var relative = relativePosition(ctxA, ctxB, a, b);

    // Finds the first ancestor with defined z-index, if any
    // The "shallowest" one is what matters, since it defined the most general
    // stacking context (affects all the descendants)
    while ( ctxA && za === undefined ) {
        ctxA = ia < 0 ? null : --ia < 0 ? a : pa[ia];
        za = zIndex(ctxA);
    }
    while ( ctxB && zb === undefined ) {
        ctxB = ib < 0 ? null : --ib < 0 ? b : pb[ib];
        zb = zIndex(ctxB);
    }

    // Compare the z-indices, if applicable; otherwise use the relative method
    if ( za !== undefined ) {
        if ( zb !== undefined )
            return za > zb ? a : za < zb ? b : relative;
        return za > 0 ? a : za < 0 ? b : relative;
    }
    else if ( zb !== undefined )
        return zb < 0 ? a : zb > 0 ? b : relative;
    else
        return relative;
}

Вот три примера, демонстрирующих этот метод на практике: Пример 1, Пример 2, Пример 3 (извините, не удосужился перевести все на английский... это точно такой же код, просто разные имена функций и переменных).

Это решение, скорее всего, неполное, и должно быть неудачно в крайних случаях (хотя я не смог найти никого). Если у кого-то есть предложения по улучшению, это было бы действительно оценено.

Ответ 3

Вы можете получить размеры и смещения элементов, а затем использовать document.elementFromPoint(), чтобы определить, какой из них является элементом, отображаемым сверху.