Come calcolare la quantità di elementi della flexbox in una riga?

Una griglia viene implementata utilizzando la flexbox CSS. Esempio:

inserisci la descrizione dell'immagine qui

Il numero di righe in questo esempio è 4 perché ho corretto la larghezza del contenitore per scopi dimostrativi. Ma, in realtà, può cambiare in base alla larghezza del contenitore (ad es. Se l’utente ridimensiona la finestra). Prova a ridimensionare la finestra Output in questo esempio per avere un’idea.

C’è sempre un object attivo, contrassegnato con il bordo nero.

Usando JavaScript, permetto agli utenti di navigare alla voce precedente / successiva usando la freccia sinistra / destra. Nella mia implementazione, faccio solo diminuire / aumentare l’indice dell’elemento attivo di 1.

Ora, vorrei consentire agli utenti di navigare su / giù pure. Per questo, ho solo bisogno di diminuire / aumentare l’indice dell’elemento attivo di . Ma come faccio a calcolare questo numero dato che dipende dalla larghezza del contenitore? C’è un modo migliore per implementare la funzionalità su / giù?

 .grid { display: flex; flex-wrap: wrap; align-content: flex-start; width: 250px; height: 200px; background-color: #ddd; padding: 10px 0 0 10px; } .item { width: 50px; height: 50px; background-color: red; margin: 0 10px 10px 0; } .active.item { outline: 5px solid black; } 
 

(Per un’esperienza ottimale, è meglio eseguire gli snippet interattivi su tutta la pagina)

Calcolo del numero di elementi per riga

È necessario ottenere la larghezza di un elemento con il suo margine (eventualmente border se sono impostati anche), quindi è necessario ottenere la larghezza interna del contenitore senza padding . Avendo questi 2 valori fai una semplice divisione per ottenere il numero di elementi per riga.

Non dimenticare di considerare il caso in cui hai solo una riga, quindi devi ottenere il valore minimo tra il numero totale di elementi e il numero che ottieni dalla divisione.

 //total number of element var n_t = document.querySelectorAll('.item').length; //width of an element var w = parseInt(document.querySelector('.item').offsetWidth); //full width of element with margin var m = document.querySelector('.item').currentStyle || window.getComputedStyle(document.querySelector('.item')); w = w + parseInt(m.marginLeft) + parseInt(m.marginRight); //width of container var w_c = parseInt(document.querySelector('.grid').offsetWidth); //padding of container var c = document.querySelector('.grid').currentStyle || window.getComputedStyle(document.querySelector('.grid')); var p_c = parseInt(c.paddingLeft) + parseInt(c.paddingRight); //nb element per row var nb = Math.min(parseInt((w_c - p_c) / w),n_t); console.log(nb); window.addEventListener('resize', function(event){ //only the width of container will change w_c = parseInt(document.querySelector('.grid').offsetWidth); nb = Math.min(parseInt((w_c - p_c) / w),n_t); console.log(nb); }); 
 .grid { display: flex; flex-wrap: wrap; resize:horizontal; align-content: flex-start; background-color: #ddd; padding: 10px 0 0 10px; } .item { width: 80px; height: 80px; background-color: red; margin: 0 10px 10px 0; } .active.item { outline: 5px solid black; } 
 

La domanda è leggermente più complessa della ricerca di quanti oggetti ci sono in fila.

In definitiva, vogliamo sapere se c’è un elemento sopra, sotto, a sinistra ea destra dell’elemento attivo. E questo deve tener conto dei casi in cui la riga inferiore è incompleta. Ad esempio, nel caso seguente, l’elemento attivo non ha elementi sopra, sotto o destra:

inserisci la descrizione dell'immagine qui

Ma, per determinare se c’è un object sopra / sotto / sinistra / destra dell’elemento attivo, dobbiamo sapere quanti oggetti ci sono in fila.

Trova il numero di articoli per riga

Per ottenere il numero di articoli per riga abbiamo bisogno di:

  • itemWidth : la outerWidth di un singolo elemento tra cui border , padding e margin
  • gridWidth – the innerWidth della griglia, esclusi border , padding e margin

Per calcolare questi due valori con un semplice JavaScript possiamo usare:

 const itemStyle = singleItem.currentStyle || window.getComputedStyle(active); const itemWidth = singleItem.offsetWidth + parseFloat(itemStyle.marginLeft) + parseFloat(itemStyle.marginRight); const gridStyle = grid.currentStyle || window.getComputedStyle(grid); const gridWidth = grid.clientWidth - (parseFloat(gridStyle.paddingLeft) + parseFloat(gridStyle.paddingRight)); 

Quindi possiamo calcolare il numero di elementi per riga usando:

 const numPerRow = Math.floor(gridWidth / itemWidth) 

Nota: questo funziona solo per articoli di dimensioni uniformi e solo se il margin è definito in unità px .

Un approccio molto, molto, molto più semplice

Occuparsi di tutte queste larghezze, paddings, margini e bordi è davvero confuso. C’è una soluzione molto, molto, molto più semplice.

Abbiamo solo bisogno di trovare l’indice dell’elemento della griglia che ha la proprietà offsetTop maggiore del primo offsetTop dell’elemento della offsetTop .

 const grid = Array.from(document.querySelector("#grid").children); const baseOffset = grid[0].offsetTop; const breakIndex = grid.findIndex(item => item.offsetTop > baseOffset); const numPerRow = (breakIndex === -1 ? grid.length : breakIndex); 

Il ternario alla fine tiene conto dei casi in cui è presente un solo elemento nella griglia e / o una singola riga di elementi.

 const getNumPerRow = (selector) => { const grid = Array.from(document.querySelector(selector).children); const baseOffset = grid[0].offsetTop; const breakIndex = grid.findIndex(item => item.offsetTop > baseOffset); return (breakIndex === -1 ? grid.length : breakIndex); } 
 .grid { display: flex; flex-wrap: wrap; align-content: flex-start; width: 400px; background-color: #ddd; padding: 10px 0 0 10px; margin-top: 5px; resize: horizontal; overflow: auto; } .item { width: 50px; height: 50px; background-color: red; margin: 0 10px 10px 0; } .active.item { outline: 5px solid black; } 
  

L’unico modo per muoversi su e giù che presenta una complicazione meno indesiderata a mia conoscenza è il conteggio delle caselle per riga e la modifica degli indici. L’unico problema è che devi calcolare il conteggio delle scatole su entrambi gli eventi di caricamento e ridimensionamento della finestra.

 var boxPerRow=0; function calculateBoxPerRow(){} window.onload = calculateBoxPerRow; window.onresize = calculateBoxPerRow; 

Ora se vuoi un modo molto semplice per ottenere il numero di scatole di fila senza preoccuparti delle dimensioni né del contenitore né delle scatole, dimentica i margini e i paddings , puoi verificare quante scatole sono allineate con la prima casella che confronta il offsetTop proprietà .

La proprietà di sola lettura HTMLElement.offsetTop restituisce la distanza dell’elemento corrente rispetto all’inizio del nodo offsetParent. [fonte: developer.mozilla.orgl ]

Puoi implementarlo come di seguito:

 function calculateBoxPerRow(){ var boxes = document.querySelectorAll('.item'); if (boxes.length > 1) { ‎ var i = 0, total = boxes.length, firstOffset = boxes[0].offsetTop; ‎ while (++i < total && boxes[i].offsetTop == firstOffset); ‎ boxPerRow = i; ‎ } } 

Esempio operativo completo:

 (function() { var boxes = document.querySelectorAll('.item'); var boxPerRow = 0, currentBoxIndex = 0; function calculateBoxPerRow() { if (boxes.length > 1) { var i = 0, total = boxes.length, firstOffset = boxes[0].offsetTop; while (++i < total && boxes[i].offsetTop == firstOffset); boxPerRow = i; } } window.onload = calculateBoxPerRow; window.onresize = calculateBoxPerRow; function focusBox(index) { if (index >= 0 && index < boxes.length) { if (currentBoxIndex > -1) boxes[currentBoxIndex].classList.remove('active'); boxes[index].classList.add('active'); currentBoxIndex = index; } } document.body.addEventListener("keyup", function(event) { switch (event.keyCode) { case 37: focusBox(currentBoxIndex - 1); break; case 39: focusBox(currentBoxIndex + 1); break; case 38: focusBox(currentBoxIndex - boxPerRow); break; case 40: focusBox(currentBoxIndex + boxPerRow); break; } }); })(); 
 .grid { display: flex; flex-wrap: wrap; align-content: flex-start; width: 50%; height: 200px; background-color: #ddd; padding: 10px 0 0 10px; } .item { width: 50px; height: 50px; background-color: red; margin: 0 10px 10px 0; } .active.item { outline: 5px solid black; } 
 
[You need to click on this page so that it can recieve the arrow keys]

Per supportare lo spostamento verso l’alto, il basso, sinistra e destra, non è necessario sapere quante caselle ci sono in una riga, devi solo calcolare se c’è una casella sopra, sotto, sinistra o destra della casella triggers .

Spostarsi a sinistra e a destra è semplice, come hai notato, basta controllare se la casella triggers ha un object previousSiblingElement nextSiblingElement o successivo. Per su e giù, è ansible utilizzare la casella triggers corrente come punto di ancoraggio e confrontarla con l’altro getBoundingClientRect() s, un metodo DOM che restituisce la geomeetria di un elemento rispetto alla finestra del browser.

Quando provi a salire, inizia dall’ancora e contrai verso il basso attraverso gli oggetti verso 0. Quando ti sposti verso il basso, inizia dall’ancora e conta fino alla fine del numero di elementi. Questo perché quando ci spostiamo verso l’alto, ci occupiamo solo delle caselle prima del box attivo, e quando scendiamo ci occupiamo solo delle caselle dopo di esso. Tutto ciò che dobbiamo cercare è una scatola che abbia la stessa posizione a sinistra con una posizione superiore o inferiore in alto.

Di seguito è riportato un esempio che ascolta un evento keydown sulla window e sposta lo stato attivo in base a quale tasto freccia è stato premuto. Potrebbe sicuramente essere reso più ASCIUTTO, ma ho diviso i quattro casi in modo da poter vedere la logica esatta in ciascuno. Puoi tenere premuti i tasti freccia in modo che la scatola si muova continuamente e puoi vedere che è molto performante. E ho aggiornato il tuo JSBin con la mia soluzione qui: http://jsbin.com/senigudoqu/1/edit?html,css,js,output

 const items = document.querySelectorAll('.item'); let activeItem = document.querySelector('.item.active'); function updateActiveItem(event) { let index; let rect1; let rect2; switch (event.key) { case 'ArrowDown': index = Array.prototype.indexOf.call(items, activeItem); rect1 = activeItem.getBoundingClientRect(); for (let i = index; i < items.length; i++) { rect2 = items[i].getBoundingClientRect(); if (rect1.x === rect2.x && rect1.y < rect2.y) { items[i].classList.add('active'); activeItem.classList.remove('active'); activeItem = items[i]; return; } } break; case 'ArrowUp': index = Array.prototype.indexOf.call(items, activeItem); rect1 = activeItem.getBoundingClientRect(); for (let i = index; i >= 0; i--) { rect2 = items[i].getBoundingClientRect(); if (rect1.x === rect2.x && rect1.y > rect2.y) { items[i].classList.add('active'); activeItem.classList.remove('active'); activeItem = items[i]; return; } } break; case 'ArrowLeft': let prev = activeItem.previousElementSibling; if (prev) { prev.classList.add('active'); activeItem.classList.remove('active'); activeItem = prev; } break; case 'ArrowRight': let next = activeItem.nextElementSibling; if (next) { next.classList.add('active'); activeItem.classList.remove('active'); activeItem = next; } break; default: return; } } window.addEventListener('keydown', updateActiveItem); 
 .grid { display: flex; flex-wrap: wrap; align-content: flex-start; background-color: #ddd; padding: 10px 0 0 10px; } .item { width: 50px; height: 50px; background-color: red; margin: 0 10px 10px 0; } .active.item { outline: 5px solid black; } 
  

Mentre puoi calcolare quale elemento stai cercando, ti suggerisco di cercare l’elemento sottostante. Il vantaggio di questo è che potrebbe funzionare anche se i tuoi elementi non hanno la stessa larghezza.

Quindi pensiamo agli attributi dell’elemento sottostante. Essenzialmente è il primo elemento con un offsetTop più offsetTop e lo stesso offsetLeft . Puoi fare qualcosa del genere per trovare l’elemento ontop:

 const active = document.querySelector('.item.active'); const all = [...document.querySelectorAll('.item')] const below = all .filter(c => c.offsetTop > active.offsetTop) .find(c => c.offsetLeft >= active.offsetLeft) const ontop = [...all].reverse() .filter(c => c.offsetTop < active.offsetTop) .find(c => c.offsetLeft >= active.offsetLeft) 

Questo esempio presuppone che il movimento finisca ai limiti. Inoltre, se ci si sposta dalla penultima all’ultima riga, ma ci sono meno colonne nell’ultima riga, si sposterà invece sull’ultima colonna dell’ultima riga.

Questa soluzione tiene traccia di righe / colonne e utilizza un object griglia per tenere traccia di dove si trovano gli elementi. Le posizioni verranno aggiornate nell’object griglia quando la pagina viene ridimensionata.

(puoi vedere l’aggiornamento del wrapping in azione in modalità a schermo intero)

 var items = document.querySelectorAll(".item"); var grid = {}; // keys: row, values: index of div in items variable var row, col, numRows; // called only onload and onresize function populateGrid() { grid = {}; var prevTop = -99; var row = -1; for(idx in items) { if(isNaN(idx)) continue; if(items[idx].offsetTop !== prevTop) { prevTop = items[idx].offsetTop; row++; grid[row] = []; } grid[row].push(idx); } setActiveRowAndCol(); numRows = Object.keys(grid).length } // changes active state from one element to another function updateActiveState(oldElem, newElem) { oldElem.classList.remove('active'); newElem.classList.add('active'); } // only called from populateGrid to get new row/col of active element (in case of wrap) function setActiveRowAndCol() { var activeIdx = -1; for(var idx in items) { if(items[idx].className == "item active") activeIdx = idx; } for(var key in grid) { var gridIdx = grid[key].indexOf(activeIdx); if(gridIdx > -1) { row = key; col = gridIdx; } } } function moveUp() { if(0 < row) { var oldElem = items[grid[row][col]]; row--; var newElem = items[grid[row][col]]; updateActiveState(oldElem, newElem); } } function moveDown() { if(row < numRows - 1) { var oldElem = items[grid[row][col]]; row++; var rowLength = grid[row].length var newElem; if(rowLength-1 < col) { newElem = items[grid[row][rowLength-1]] col = rowLength-1; } else { newElem = items[grid[row][col]]; } updateActiveState(oldElem, newElem); } } function moveLeft() { if(0 < col) { var oldElem = items[grid[row][col]]; col--; var newElem = items[grid[row][col]]; updateActiveState(oldElem, newElem); } } function moveRight() { if(col < grid[row].length - 1) { var oldElem = items[grid[row][col]]; col++; var newElem = items[grid[row][col]]; updateActiveState(oldElem, newElem); } } document.onload = populateGrid(); window.addEventListener("resize", populateGrid); document.addEventListener('keydown', function(e) { e = e || window.event; if (e.keyCode == '38') { moveUp(); } else if (e.keyCode == '40') { moveDown(); } else if (e.keyCode == '37') { moveLeft(); } else if (e.keyCode == '39') { moveRight(); } }); 
 .grid { display: flex; flex-wrap: wrap; resize: horizontal; align-content: flex-start; background-color: #ddd; padding: 10px 0 0 10px; } .item { width: 50px; height: 50px; background-color: red; margin: 0 10px 10px 0; } .active.item { outline: 5px solid black; } 
 

So che questo non è esattamente ciò che l’OP sta chiedendo, ma volevo mostrare una ansible alternativa (dipende dal caso d’uso).

Invece di usare la flexbox CSS, c’è anche la griglia CSS più recente che contiene effettivamente colonne e righe. Pertanto, convertendo la struttura in una griglia e utilizzando alcuni JS per ascoltare i pulsanti dei tasti premuti, è ansible spostare l’elemento attivo (vedere l’esempio di lavoro incompleto in basso).

 var x = 1, y = 1; document.addEventListener('keydown', function(event) { const key = event.key; // "ArrowRight", "ArrowLeft", "ArrowUp", or "ArrowDown" console.log(key); if (key == "ArrowRight") { x++; } if (key == "ArrowLeft") { x--; if (x < 1) { x = 1; } } if (key == "ArrowUp") { y--; if (y < 1) { y = 1; } } if (key == "ArrowDown") { y++; } document.querySelector('.active').style.gridColumnStart = x; document.querySelector('.active').style.gridRowStart = y; }); 
 .grid { display: grid; grid-template-columns: repeat(auto-fill,50px); grid-template-rows: auto; grid-gap: 10px; width: 250px; height: 200px; background-color: #ddd; padding: 10px; } .item { width: 50px; height: 50px; background-color: red; margin: 0 10px 10px 0; display: flex; justify-content: center; align-items: center; } .active { outline: 5px solid black; grid-column-start: 1; grid-column-end: span 1; grid-row-start: 1; grid-row-end: span 1; } 
 
A1
A2
A3
A4
B1
B2
B3
B4
C1
C2

offsetTop è un metodo popolare per determinare la posizione y di un elemento.

Se due elementi fratelli adiacenti hanno la stessa posizione y, possiamo tranquillamente supporre che siano visivamente sulla stessa riga (poiché tutti gli elementi hanno la stessa altezza).

Quindi, possiamo iniziare a contare il numero di elementi in una fila confrontando le loro posizioni y una per una. Smettiamo di contare non appena finiamo gli elementi o incontriamo un fratello adiacente con una diversa posizione y.

 function getCountOfItemsInRow() { let grid = document.getElementById('grid').children; //assumes #grid exists in dom let n = 0; // Zero items when grid is empty // If the grid has items, we assume the 0th element is in the first row, and begin counting at 1 if (grid.length > 0) { n = 1; // While the nth item has the same height as the previous item, count it as an item in the row. while (grid[n] && grid[n].offsetTop === grid[n - 1].offsetTop) { n++; } } return n; } 

Questo esempio presuppone che il movimento finisca ai limiti. Inoltre, se ci si sposta dalla penultima all’ultima riga, ma ci sono meno colonne nell’ultima riga, si sposterà invece sull’ultima colonna dell’ultima riga.

Questa soluzione tiene traccia di righe / colonne e utilizza un object griglia per tenere traccia di dove si trovano gli elementi.

 var items = document.querySelectorAll(".item"); var grid = {}; // keys: row, values: index of div in items variable var row, col, numRows; // called only onload and onresize function populateGrid() { grid = {}; var prevTop = -99; var row = -1; for(idx in items) { if(isNaN(idx)) continue; if(items[idx].offsetTop !== prevTop) { prevTop = items[idx].offsetTop; row++; grid[row] = []; } grid[row].push(idx); } setActiveRowAndCol(); numRows = Object.keys(grid).length } // changes active state from one element to another function updateActiveState(oldElem, newElem) { oldElem.classList.remove('active'); newElem.classList.add('active'); } // only called from populateGrid to get new row/col of active element (in case of wrap) function setActiveRowAndCol() { var activeIdx = -1; for(var idx in items) { if(items[idx].className == "item active") activeIdx = idx; } for(var key in grid) { var gridIdx = grid[key].indexOf(activeIdx); if(gridIdx > -1) { row = key; col = gridIdx; } } } function moveUp() { if(0 < row) { var oldElem = items[grid[row][col]]; row--; var newElem = items[grid[row][col]]; updateActiveState(oldElem, newElem); } } function moveDown() { if(row < numRows - 1) { var oldElem = items[grid[row][col]]; row++; var rowLength = grid[row].length var newElem; if(rowLength-1 < col) { newElem = items[grid[row][rowLength-1]] col = rowLength-1; } else { newElem = items[grid[row][col]]; } updateActiveState(oldElem, newElem); } } function moveLeft() { if(0 < col) { var oldElem = items[grid[row][col]]; col--; var newElem = items[grid[row][col]]; updateActiveState(oldElem, newElem); } } function moveRight() { if(col < grid[row].length - 1) { var oldElem = items[grid[row][col]]; col++; var newElem = items[grid[row][col]]; updateActiveState(oldElem, newElem); } } document.onload = populateGrid(); window.addEventListener("resize", populateGrid); document.addEventListener('keydown', function(e) { e = e || window.event; if (e.keyCode == '38') { moveUp(); } else if (e.keyCode == '40') { moveDown(); } else if (e.keyCode == '37') { moveLeft(); } else if (e.keyCode == '39') { moveRight(); } }); 
 .grid { display: flex; flex-wrap: wrap; resize: horizontal; align-content: flex-start; background-color: #ddd; padding: 10px 0 0 10px; } .item { width: 50px; height: 50px; background-color: red; margin: 0 10px 10px 0; } .active.item { outline: 5px solid black; } 
 

Se stai usando Jquery e sei sicuro che gli oggetti della griglia siano allineati verticalmente, questo potrebbe fare il trucco …

Non l’ho provato, ma dovrebbe funzionare (contando le colonne)

 function countColumns(){ var objects = $(".grid-object"); // choose a unique class name here var columns = [] for(var i=0;i 

Potresti usare Array.prototype.filter () per farlo in modo abbastanza ordinato. Utilizzare la funzione per ottenere la quantità di elementi in una riga. Passare nel selettore CSS che si desidera utilizzare (in questo caso .item). Una volta ottenuta la dimensione della riga, la navigazione con la freccia è facile.

 function getRowSize( cssSelector ) { var firstTop = document.querySelector( cssSelector ).offsetTop; // Sets rowArray to be an array of the nodes (divs) in the 1st row. var rowArray = Array.prototype.filter.call(document.querySelectorAll( cssSelector ), function(element){ if( element.offsetTop == firstTop ) return element; }); // Return the amount of items in a row. return rowArray.length; } 

Esempi

Demo CodePen: https://codepen.io/gtlitc/pen/EExXQE

Demo interattivo che mostra le dimensioni della riga e gli importi delle mosse. http://www.smallblue.net/demo/49043684/

Spiegazione

Innanzitutto, la funzione imposta una variabile firstTop in modo che sia l’ offsetTop del primo nodo.

Successivamente la funzione crea un array rowArray di nodes nella prima riga (se è ansible la navigazione su e giù la prima riga sarà sempre una riga intera).

Questo viene fatto chiamando (prendendo in prestito) la funzione filtro dal Prototipo di Array. We cant simply call the filter function on the node list that is returned by the QSA (query selector all) because browsers return node lists instead of arrays and node lists are not proper arrays.

The if statement then simply filters all of the nodes and only returns the ones that have the same offsetTop as the first node. ie all of the nodes in the first row.

We now have an array from which we can determine the length of a row.

I have omitted the implementation of the DOM traversal as this is simple using either pure Javascript or Jquery etc and was not part of the OPs question. I would only note that it is important to test if the element you intend to move to exists before moving there.

This function will work with any layout technique. Flexbox, float, CSS grid, whatever the future holds.

Riferimenti

Why does document.querySelectorAll return a StaticNodeList rather than a real Array?

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter