2012-08-30 15 views
10

Dato due elementi HTML A e B in abile nello stesso documento, come posso scoprire quale è "più vicino" all'utente (cioè se si sovrappongono, quale oscura l'altro)?Confronta gli elementi HTML con lo z-index effettivo

Il W3C CSS Specification descrive i contesti di impilamento, che devono essere implementati dai motori di rendering conformi. Tuttavia, non sono riuscito a trovare un modo per accedere a queste informazioni in un programma JavaScript, cross-browser o no. Tutto quello che posso leggere è la proprietà css z-index, che di per sé non dice molto, poiché la maggior parte del tempo è impostata su auto o, anche se espressa come valore numerico, non è un indicatore affidabile di come viene effettivamente visualizzata (se appartengono a diversi contesti statcking, il confronto tra gli z-index è irrilevante).

Si prega di notare che sono interessato a arbitrario elementi: se entrambi gli elementi sono sotto il puntatore del mouse, solo uno sarà considerato "al passaggio del mouse", quindi posso facilmente trovare quello più vicino in questo caso. Tuttavia, sto cercando una soluzione più generale, preferibilmente una che non implichi la re-implementazione dell'algoritmo di stacking che il motore di rendering sta già eseguendo.

Aggiornamento: vorrei chiarire un po 'la ragione dietro a questa domanda: di recente ho affrontato a question che ha esposto una limitazione nel meccanismo di trascinamento e rilascio di jQuery - non ci vuole z-indici in considerazione quando si cadere, quindi se un l'elemento oscura un altro, può comunque eseguire l'operazione di rilascio nell'elemento "dietro". Mentre la domanda collegata è stata risolta per il caso particolare del PO, il problema generale persiste e non c'è una soluzione facile che io conosca.

alex's answer sotto è utile, ma non sufficiente per il caso in questione: durante il trascinamento, l'elemento trascinato stessa (o più precisamente il suo helper) è l'elemento più in alto sotto il cursore del mouse, quindi elementFromPoint sarà tornare esso anziché il prossimo elemento, quello di cui abbiamo veramente bisogno (soluzione alternativa:style the cursor quindi è posizionato all'esterno dell'assistente). Le altre strategie di intersezione utilizzate da jQuery tengono conto anche di più di un punto, complicando il compito di determinare l'elemento più in alto che interseca l'helper in qualche modo. Essere in grado di confrontare (o ordinare) gli elementi con lo z-index effettivo renderebbe valida una modalità di intersezione "z-index aware" per il caso generale.

risposta

1

Dopo alcuni giorni di ricerca, penso di aver reimplementato con successo il meccanismo di impilamento secondo le regole del 2016. Ho sostanzialmente aggiornato l'approccio 2013 (pubblicato dall'OP). Il risultato è una funzione che confronta due nodi DOM e restituisce quello che è visivamente in primo piano.

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

ragionamento

Ci sono altri modi per determinare quale sia l'elemento sopra l'altro. Ad esempio: document.elementFromPoint() o document.elementsFromPoint(). Tuttavia, ci sono molti fattori (non documentati) che influenzano l'affidabilità di questi metodi. Ad esempio, opacità, visibilità, eventi puntatore, visibilità sul retro e alcune trasformazioni potrebbero rendere document.elementFromPoint() incapace di colpire un elemento specifico. E poi c'è il problema che document.elementFromPoint() può interrogare solo l'elemento più in alto (non quelli sottostanti). Questo dovrebbe essere risolto con document.elementsFromPoint(), ma attualmente è stato implementato solo in Chrome. In aggiunta a ciò, ho presentato un bug agli sviluppatori di Chrome su document.elementsFromPoint(). Quando si colpisce un tag di ancoraggio, tutti gli elementi sottostanti passano inosservati.

Tutti questi problemi combinati mi hanno indotto a tentare una reimplementazione del meccanismo di impilamento. Il vantaggio di questo approccio è che il meccanismo di impilamento è ampiamente documentato e che può essere testato e compreso.

Come funziona

Il mio approccio ri-implementa il meccanismo HTML accatastamento. Mira a seguire correttamente tutte le regole che influenzano l'ordine di impilamento degli elementi HTML. Questo include regole di posizionamento, float, ordine DOM ma anche proprietà CSS3 come opacità, trasformazione e altre proprietà sperimentali come filtro e maschera. Le regole sembrano essere correttamente implementate a partire da marzo 2016, ma dovranno essere aggiornate in futuro quando cambiano le specifiche e il supporto del browser.

Ho messo tutto insieme in un GitHub repository. Speriamo che questo approccio continui a funzionare in modo affidabile. Ecco un example JSFiddle del codice in azione. Nell'esempio tutti gli elementi vengono ordinati per l'effettivo 'z-index', che è ciò che l'OP stava cercando.

Test e feedback su questo approccio sarebbe molto gradito!

+0

Testerò il tuo codice con più attenzione quando avrò tempo, ma a prima vista sembra soddisfacente. Per ora accetto la tua risposta, poiché è la più completa che abbiamo finora. – mgibsonbr

2

È possibile ottenere le dimensioni e gli offset degli elementi, quindi utilizzare document.elementFromPoint() per determinare quale è l'elemento visualizzato sopra.

+0

Questo è molto utile! Speravo in un metodo che non richiedesse un dato punto, ma per molti scopi pratici questo offre davvero una buona soluzione. – mgibsonbr

3

Nota: dopo più di un anno senza una risposta, questa domanda era also posted at Stack Overflow in Portuguese e - pur senza una soluzione definitiva - alcuni utenti e me erano in grado di replicate the stacking mechanism in JavaScript (reinventare la ruota, ma ancora ...)

Citando l'algoritmo contesto di stratificazione al CSS2 specification (sottolineatura mia):

l'elemento principale costituisce il contesto principale impilamento. Altri contesti di impilamento sono generati da qualsiasi elemento posizionato (inclusi gli elementi relativamente posizionati) con un valore calcolato di 'z-index' diverso da 'auto'. I contesti di impilamento non sono necessariamente correlati ai blocchi di contenimento.In futuri livelli dei CSS, altre proprietà possono introdurre contesti di impilamento, ad esempio 'opacità'

Da questa descrizione, ecco una funzione per restituire: a) la z-index di un elemento, se si genera un nuovo accatastamento contex; oppure b) undefined se esso non>

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); 
} 

Questo dovrebbe essere in grado di impostare a parte elementi che formano diversi contesti di stratificazione. Per il resto degli elementi (e per gli elementi con un pari z-index) il Appendix E dice che dovrebbero rispettare "ordine albero":

Preorder in profondità attraversamento dell'albero di rendering, in logica (non visivo) Affinché contenuto bidirezionale, dopo aver preso in considerazione le proprietà che spostano le caselle.

Fatta eccezione per quei "proprietà che si muovono le scatole intorno", questa funzione shoud implementa correttamente l'attraversamento:

/* 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); 
} 

Con queste due funzioni definite, possiamo finalmente creare la nostra funzione di confronto elemento:

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; 
} 

Ecco tre esempi che mostrano questo metodo nella pratica: Example 1, Example 2, Example 3 (scusate, non si è preoccupato di tradurre tutto in e inglese ... è lo stesso identico codice, solo funzioni e nomi di variabili diversi).

Questa soluzione è molto probabilmente incompleta e dovrebbe fallire nei casi limite (anche se non sono riuscito a trovarla). Se qualcuno ha qualche suggerimento per miglioramenti, sarebbe davvero apprezzato.