グリッドは、CSSフレックスボックスを使用して実装されます。 例:
この例の行数は4です。これは、デモの目的でコンテナーの幅を固定したためです。ただし、実際には、コンテナの幅に基づいて変更できます(たとえば、ユーザーがウィンドウのサイズを変更した場合)。 この例 で出力ウィンドウのサイズを変更してみてください。
黒い境界線でマークされたアクティブなアイテムが常に1つあります。
JavaScriptを使用して、ユーザーが左/右矢印を使用して前/次の項目に移動できるようにします。私の実装では、アクティブアイテムのインデックスを1ずつ増減します。
ここで、ユーザーが上下にナビゲートできるようにしたいと思います。そのためには、アクティブなアイテムのインデックスを<amount of items in a row>
だけ増減する必要があります。しかし、コンテナの幅に依存している場合、この数値をどのように計算すればよいですか?アップ/ダウン機能を実装するより良い方法はありますか?
.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;
}
<div id="grid" class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
質問は、一列にいくつのアイテムがあるかを見つけるよりも少し複雑です。
最終的に、アクティブな要素の上、下、左、右に要素があるかどうかを知りたいです。そして、これは一番下の行が不完全な場合を考慮する必要があります。たとえば、次の場合、アクティブな要素には上、下、または右のアイテムがありません。
ただし、アクティブなアイテムの上/下/左/右にアイテムがあるかどうかを判断するには、行にあるアイテムの数を知る必要があります。
行ごとのアイテム数を取得するには、次が必要です。
itemWidth
-outerWidth
、border
、およびpadding
を含む単一要素のmargin
gridWidth
-グリッドのinnerWidth
、border
、padding
、およびmargin
を除くこれらの2つの値をプレーンなJavaScriptで計算するには、次を使用できます。
_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));
_
次に、次を使用して行ごとの要素数を計算できます。
_const numPerRow = Math.floor(gridWidth / itemWidth)
_
注:これは均一サイズのアイテムでのみ機能し、margin
がpx
単位で定義されている場合のみです。
これらのすべての幅、パディング、マージン、および境界線を処理するのは本当に混乱します。はるかに簡単な解決策があります。
offsetTop
プロパティのグリッド要素のインデックスを見つける必要があるのは、最初のグリッド要素の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);
_
最後の3項は、グリッドにアイテムが1つしかない場合や、アイテムの行が1つしかない場合に対応します。
_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;
}
_
_<button onclick="alert(getNumPerRow('#grid'))">Get Num Per Row</button>
<div id="grid" class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
_
アクティブな要素の上または下にアイテムがあるかどうかを知るには、3つのパラメーターを知る必要があります。
totalItemsInGrid
activeIndex
numPerRow
たとえば、次の構造では:
_<div id="grid" class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
</div>
_
_5
_のtotalItemsInGrid
があり、activeIndex
の_2
_(グループ内の3番目の要素)のゼロベースのインデックスがあり、numPerRow
が3だとします。
これで、アクティブなアイテムの上、下、左、または右にアイテムがあるかどうかを判断できます。
isTopRow = activeIndex <= numPerRow - 1
_isBottomRow = activeIndex >= totalItemsInGid - numPerRow
_isLeftColumn = activeIndex % numPerRow === 0
_isRightColumn = activeIndex % numPerRow === numPerRow - 1 || activeIndex === gridNum - 1
_isTopRow
がtrue
の場合、上に移動することはできません。また、isBottomRow
がtrue
の場合、下に移動することはできません。 isLeftColumn
がtrue
の場合、左に移動できません。また、isRightColumn
の場合、true
の場合、右に移動できません。
注:isBottomRow
は、アクティブな要素が一番下の行にあるかどうかをチェックするだけでなく、その下に要素があるかどうかもチェックします。上記の例では、アクティブな要素はnotですが、下の行にはありませんその下のアイテム。
これをリサイズで機能する完全な例に取り組んでいます。また、_#grid
_要素のサイズを変更可能にして、以下のスニペットでテストできるようにしました。
3つのパラメーターを受け入れる関数navigateGrid
を作成しました。
gridSelector
-グリッド要素のDOMセレクターactiveClass
-アクティブな要素のクラス名direction
-up
、down
、left
、またはright
のいずれかこれは、質問のHTML構造で'navigateGrid("#grid", "active", "up")
として使用できます。
この関数は、offset
メソッドを使用して行数を計算し、active
要素をup/down/left/right要素に変更できるかどうかを確認します。
言い換えると、この関数は、アクティブな要素を上下および左右に移動できるかどうかをチェックします。これの意味は:
_const navigateGrid = (gridSelector, activeClass, direction) => {
const grid = document.querySelector(gridSelector);
const active = grid.querySelector(`.${activeClass}`);
const activeIndex = Array.from(grid.children).indexOf(active);
const gridChildren = Array.from(grid.children);
const gridNum = gridChildren.length;
const baseOffset = gridChildren[0].offsetTop;
const breakIndex = gridChildren.findIndex(item => item.offsetTop > baseOffset);
const numPerRow = (breakIndex === -1 ? gridNum : breakIndex);
const updateActiveItem = (active, next, activeClass) => {
active.classList.remove(activeClass);
next.classList.add(activeClass);
}
const isTopRow = activeIndex <= numPerRow - 1;
const isBottomRow = activeIndex >= gridNum - numPerRow;
const isLeftColumn = activeIndex % numPerRow === 0;
const isRightColumn = activeIndex % numPerRow === numPerRow - 1 || activeIndex === gridNum - 1;
switch (direction) {
case "up":
if (!isTopRow)
updateActiveItem(active, gridChildren[activeIndex - numPerRow], activeClass);
break;
case "down":
if (!isBottomRow)
updateActiveItem(active, gridChildren[activeIndex + numPerRow], activeClass);
break;
case "left":
if (!isLeftColumn)
updateActiveItem(active, gridChildren[activeIndex - 1], activeClass);
break;
case "right":
if (!isRightColumn)
updateActiveItem(active, gridChildren[activeIndex + 1], activeClass);
break;
}
}
_
_.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;
}
_
_<button onClick='navigateGrid("#grid", "active", "up")'>Up</button>
<button onClick='navigateGrid("#grid", "active", "down")'>Down</button>
<button onClick='navigateGrid("#grid", "active", "left")'>Left</button>
<button onClick='navigateGrid("#grid", "active", "right")'>Right</button>
<div id="grid" class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
_
(最適なエクスペリエンスのために、全ページでインタラクティブなスニペットをよりよく実行する)
行ごとの要素数の計算
its margin (最終的にも設定されている場合は境界線)で要素の幅を取得する必要があります。その後、内側の幅を取得する必要がありますコンテナのパディングなし。これらの2つの値を使用して、単純な除算を行い、行ごとの要素の数を取得します。
行が1行しかない場合を考慮することを忘れないでください。したがって、要素の合計数と部門から取得する数の間の最小値を取得する必要があります。
_//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;
}
_
_<div id="grid" class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
_
以下は、同じロジックのjQueryバージョンで、コードが少なくなっています。
_//total number of element
var n_t = $('.item').length;
//full width of element with margin
var w = $('.item').outerWidth(true);
//width of container without padding
var w_c = $('.grid').width();
//nb element per row
var nb = Math.min(parseInt(w_c / w),n_t);
console.log(nb);
window.addEventListener('resize', function(event){
//only the width of container will change
w_c = $('.grid').width();
nb = Math.min(parseInt(w_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;
}
_
_<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="grid" class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
_
そして、ここにインタラクティブなグリッドのデモがあります:
_var all = document.querySelectorAll('.item');
var n_t = all.length;
var current = 0;
all[current].classList.add('active');
var w = parseInt(document.querySelector('.item').offsetWidth);
var m = document.querySelector('.item').currentStyle || window.getComputedStyle(document.querySelector('.item'));
w = w + parseInt(m.marginLeft) + parseInt(m.marginRight);
var w_c = parseInt(document.querySelector('.grid').offsetWidth);
var c = document.querySelector('.grid').currentStyle || window.getComputedStyle(document.querySelector('.grid'));
var p_c = parseInt(c.paddingLeft) + parseInt(c.paddingRight);
var nb = Math.min(parseInt((w_c - p_c) / w),n_t);
window.addEventListener('resize', function(e){
w_c = parseInt(document.querySelector('.grid').offsetWidth);
nb = Math.min(parseInt((w_c - p_c) / w),n_t);
});
document.addEventListener('keydown',function (e) {
e = e || window.event;
if (e.keyCode == '38') {
if(current - nb>=0) {
all[current].classList.remove('active');
current-=nb;
all[current].classList.add('active');
}
}
else if (e.keyCode == '40') {
if(current + nb<n_t) {
all[current].classList.remove('active');
current+=nb;
all[current].classList.add('active');
}
}
else if (e.keyCode == '37') {
if(current>0) {
all[current].classList.remove('active');
current--;
all[current].classList.add('active');
}
}
else if (e.keyCode == '39') {
if(current<n_t-1) {
all[current].classList.remove('active');
current++;
all[current].classList.add('active');
}
}
});
_
_.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;
}
_
_<div id="grid" class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
_
行ごとの要素数を必要とせずにグリッド内をナビゲートする別の方法も検討できます。アイデアは、関数 elementFromPoint(x,y)
に依存することです。
ロジックは次のとおりです。アクティブな要素内にあり、その_(x,y)
_位置があります。キーを押すことにより、これらの値を増減し、上記の関数を使用して、新しい_(x,y)
_を使用して新しい要素を取得します。有効な要素を取得し、この要素がアイテム(item
クラスを含む)かどうかをテストします。この場合、前のものからアクティブを削除し、新しいものに追加します。
これは、 inside ナビゲーションのみを検討する例です。コンテナの左/右の境界に到達すると、前/次の行に到達しません。
_var a = document.querySelector('.item');
a.classList.add('active');
var off = a.getBoundingClientRect();
/* I get the center position to avoid any potential issue with boundaries*/
var y = off.top + 40;
var x = off.left + 40;
document.addEventListener('keydown', function(e) {
e = e || window.event;
if (e.keyCode == '38') {
var elem = document.elementFromPoint(x, y - 90 /* width + both margin*/);
if (elem &&
elem.classList.contains('item')) {
document.querySelector('.active').classList.remove('active');
elem.classList.add('active');
y -= 90;
}
} else if (e.keyCode == '40') {
var elem = document.elementFromPoint(x, y + 90);
if (elem &&
elem.classList.contains('item')) {
document.querySelector('.active').classList.remove('active');
elem.classList.add('active');
y += 90;
}
} else if (e.keyCode == '37') {
var elem = document.elementFromPoint(x - 90, y);
if (elem &&
elem.classList.contains('item')) {
document.querySelector('.active').classList.remove('active');
elem.classList.add('active');
x -= 90;
}
} else if (e.keyCode == '39') {
var elem = document.elementFromPoint(x + 90, y);
if (elem &&
elem.classList.contains('item')) {
document.querySelector('.active').classList.remove('active');
elem.classList.add('active');
x += 90;
}
}
});
window.addEventListener('resize', function(e) {
var off = document.querySelector('.active').getBoundingClientRect();
y = off.top + 40;
x = off.left + 40;
});
_
_.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;
}
_
_<div id="grid" class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
_
このメソッドでお気づきかもしれませんが、コンテナ、画面サイズ、要素数などに関する情報は必要ありません。必要な情報は、単一のアイテムの寸法だけです。また、ウィンドウのサイズ変更時にアクティブな要素の位置を修正するための小さなコードが必要です。
クラスを追加したりJSで取得したりせずに visually アクティブな要素を取得したい場合の別のfancyアイデアがあります。コンテナの背景を使用して、アクティブな要素の背後にブラックボックスを作成するという考え方です。
ところで、この方法には2つの欠点があります。
以下は、高さ/幅が固定されたコンテナを使用した簡略化されたコードです。
_var grid = document.querySelector('.grid');
document.addEventListener('keydown', function(e) {
e = e || window.event;
if (e.keyCode == '38') {
var y = parseInt(grid.style.backgroundPositionY);
y= (y-90 + 270)%270;
grid.style.backgroundPositionY=y+"px";
} else if (e.keyCode == '40') {
var y = parseInt(grid.style.backgroundPositionY);
y= (y+90)%270;
grid.style.backgroundPositionY=y+"px";
} else if (e.keyCode == '37') {
var x = parseInt(grid.style.backgroundPositionX);
x= (x-90 + 270)%270;
grid.style.backgroundPositionX=x+"px";
} else if (e.keyCode == '39') {
var x = parseInt(grid.style.backgroundPositionX);
x= (x+90)%270;
grid.style.backgroundPositionX=x+"px";
}
});
_
_.grid {
display: flex;
flex-wrap: wrap;
width:270px;
resize: horizontal;
align-content: flex-start;
background-color: #ddd;
padding: 10px 0 0 10px;
background-image:linear-gradient(#000,#000);
background-size:90px 90px;
background-repeat:no-repeat;
}
.item {
width: 80px;
height: 80px;
background-color: red;
margin: 0 10px 10px 0;
}
_
_<div id="grid" class="grid" style="background-position:5px 5px;">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
_
ご覧のとおり、コードは非常に単純なので、ほとんどすべての値が既知で修正されているような状況に適しています。
私の知る限り、不要な複雑さを軽減するために上下に移動する唯一の方法は、行ごとのボックスの数とインデックスの変更です。唯一の問題は、ウィンドウの読み込みイベントとサイズ変更イベントの両方でボックスカウントを計算する必要があることです。
var boxPerRow=0;
function calculateBoxPerRow(){}
window.onload = calculateBoxPerRow;
window.onresize = calculateBoxPerRow;
これで、行のボックス数を取得する非常に簡単な方法が必要な場合サイズを気にせずに/コンテナとボックスのどちらでもない、マージンとパディングを忘れる、ボックスの数を確認できます最初のボックスに揃えられますoffsetTopプロパティを比較します。
HTMLElement.offsetTop read-onlyプロパティは、offsetParentノードの上部を基準とした現在の要素の距離を返します。 [ソース: developer.mozilla.orgl ]
以下のように実装できます:
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;
}
}
完全に機能する例:
(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;
}
<div>[You need to click on this page so that it can recieve the arrow keys]</div>
<div id="grid" class="grid">
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
上下左右の移動をサポートするには、行にあるボックスの数を知る必要はなく、アクティブなボックスの上、下、左、または右のボックスがあるかどうかを計算するだけです。
お気づきのとおり、左右に移動するのは簡単です。アクティブボックスにpreviousSiblingElement
またはnextSiblingElement
があるかどうかを確認するだけです。上下の場合、現在のアクティブなボックスをアンカーポイントとして使用し、それを他のボックスのgetBoundingClientRect()
sと比較できます。これは、要素の相対ジオメトリを返す DOMメソッド です。ブラウザのビューポートに。
上に移動しようとするときは、アンカーから開始してアイテムを0に向かってカウントダウンします。下に移動するときは、アンカーから開始してアイテムの数が終わるまでカウントします。これは、上に移動する場合、アクティブなボックスの前のボックスのみを考慮し、下に移動する場合、その後のボックスのみを考慮するためです。私たちが探す必要があるのは、同じ左位置に、より高いまたはより低い上部位置を持つ箱だけです。
以下は、window
のkeydownイベントをリッスンし、どの矢印キーが押されたかに応じてアクティブ状態を移動する例です。間違いなくもっと乾燥させることができますが、4つのケースを分割して、それぞれの正確なロジックを確認できるようにしました。矢印キーを押し続けると、ボックスが連続して移動し、非常にパフォーマンスが高いことがわかります。そして、ここで私のソリューションであなたのJSBinを更新しました: 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;
}
<div id="grid" class="grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
探している要素を計算できますが、以下の要素を検索することをお勧めします。この利点は、要素の幅が同じでない場合でも機能することです。
それでは、以下の要素の属性について考えてみましょう。基本的に、offsetTop
と同じoffsetLeft
を持つ最初の要素。上の要素を見つけるために次のようなことができます:
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)
この例では、境界で動作が終了することを想定しています。また、2番目から最後の行から最後の行に移動するが、最後の行の列が少ない場合、代わりに最後の行の最後の列に移動します。
このソリューションでは、行/列を追跡し、グリッドオブジェクトを使用して要素の場所を追跡します。ページのサイズが変更されると、グリッドオブジェクトの位置が更新されます。
(フルスクリーンモードで動作中のラッピングの更新を確認できます)
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;
}
<div id="grid" class="grid">
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
Jqueryを使用していて、グリッドオブジェクトが垂直方向に整列していると確信している場合は、これでうまくいきます。
私はそれをテストしませんでしたが、動作するはずです(列を数えることによって)
function countColumns(){
var objects = $(".grid-object"); // choose a unique class name here
var columns = []
for(var i=0;i<objects.length;i++){
var pos = $(objects[i]).position().left
if(columns.indexOf(pos) < 1) columns.Push(pos);
}
return columns.length
}
Array.prototype.filter()を使用して、これを非常にきれいに行うことができます。行内のアイテムの量を取得するには、この関数を使用します。使用するCSSセレクター(この場合は.item)を渡します。行のサイズが決まったら、矢印のナビゲーションは簡単です。
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;
}
例
CodePenデモ: https://codepen.io/gtlitc/pen/EExXQE
行サイズと移動量を表示するインタラクティブなデモ。 http://www.smallblue.net/demo/49043684/
説明
最初に、関数は変数firstTop
を最初のノードのoffsetTop
に設定します。
次に、関数は最初の行にノードの配列rowArray
を構築します(上下のナビゲーションが可能な場合、最初の行は常に完全な長さの行になります)。
これは、Array Prototypeからフィルター関数を呼び出す(借りる)ことによって行われます。ブラウザは配列の代わりにノードリストを返し、ノードリストは適切な配列ではないため、QSA(クエリセレクターall)によって返されるノードリストのフィルター関数を単に呼び出すことはできません。
Ifステートメントは、すべてのノードを単純にフィルター処理し、最初のノードと同じoffsetTop
を持つノードのみを返します。つまり、最初の行のすべてのノード。
これで、行の長さを決定できる配列ができました。
DOMトラバーサルの実装は省略しました。これは、純粋なJavascriptまたはJqueryなどを使用して簡単であり、OPの質問の一部ではないためです。移動する前に、移動先の要素が存在するかどうかをテストすることが重要であることに注意してください。
この関数は、任意のレイアウト手法で機能します。フレックスボックス、フロート、CSSグリッド、未来が何であれ。
参考文献
document.querySelectorAllが実際の配列ではなくStaticNodeListを返すのはなぜですか?
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
私はこれがOPが求めているものとはまったく異なることを知っていますが、可能な代替案を示したかったのです(ユースケースによって異なります)。
CSSフレックスボックスを使用する代わりに、実際には列と行を備えた最新のCSSグリッドもあります。したがって、構造をグリッドに変換し、JSを使用して、押されているキーボタンをリッスンすることにより、アクティブなアイテムを移動できます(以下の不完全な作業例を参照)。
_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;
}
_
_<div id="grid" class="grid">
<div class="item active">A1</div>
<div class="item">A2</div>
<div class="item">A3</div>
<div class="item">A4</div>
<div class="item">B1</div>
<div class="item">B2</div>
<div class="item">B3</div>
<div class="item">B4</div>
<div class="item">C1</div>
<div class="item">C2</div>
</div>
_
ただし、前述のように、このソリューションには欠点があります。一時的に、アクティブなアイテムは実際にはそれ自体がグリッドアイテムであり、グリッドに沿って移動し、他の要素がその周りを流れます。第二に、flexboxモデルと同様に、現在、グリッド位置に基づいてアイテムをターゲットにするCSSセレクタはありません。
ただし、とにかくJavaScriptを使用しているため、すべてのグリッド項目をループして、CSSグリッドプロパティを取得できます。それらが現在の座標と一致する場合、ターゲット要素があります。悲しいことに、これは各要素が配置されている場合にのみ機能し、要素に_grid-column-start: auto
_を使用しても役に立ちません。 window.getComputedStyle()
でさえ、auto
のみを返します。
offsetTop
は、要素のy位置を決定する一般的な方法です。
2つの隣接する兄弟要素が同じy位置にある場合、それらは視覚的に同じ行にあると安全に想定できます(すべての要素の高さが同じであるため)。
したがって、y位置を1つずつ比較することで、行の要素数のカウントを開始できます。要素がなくなるか、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;
}
この例では、境界で動作が終了することを想定しています。また、2番目から最後の行から最後の行に移動するが、最後の行の列が少ない場合、代わりに最後の行の最後の列に移動します。
このソリューションでは、行/列を追跡し、グリッドオブジェクトを使用して要素の場所を追跡します。
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;
}
<div id="grid" class="grid">
<div class="item active"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>