Järjestä raahaamalla tai painikkeilla.
Kahvi
↑
↓
Tee
↑
↓
Kaakao
↑
↓
CSS
/* Esikatselualue määrittelee sekä taustan että tekstin värin, jotta pari on varmasti luettava kummassakin teemassa. */ #alue { background-color: #ffffff; color: #1a1a1a; padding: 0.5rem; } @media (prefers-color-scheme: dark) { #alue { background-color: #1e1e1e; color: #f0f0f0; } } #lista { list-style: none; padding: 0; max-width: 24rem; } #lista li { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; margin-bottom: 0.3rem; border: 2px solid currentColor; cursor: grab; /* estää selaimen omat eleet kortin päällä kosketusnäytöillä. Tätä ei voi asettaa javascriptilla. */ touch-action: none; } #lista li.raahataan { opacity: 0.4; } #lista li span { flex-grow: 1; } /* Ruudunlukijalle tarkoitettu alue: ei näy, mutta luetaan. display: none piilottaisi sen myös apuvälineiltä. */ .vain-ruudunlukijalle { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
HTML
<div id="alue"> <p>Järjestä raahaamalla tai painikkeilla.</p> <ul id="lista"> <li><span>Kahvi</span> <button type="button" class="ylos">↑</button> <button type="button" class="alas">↓</button></li> <li><span>Tee</span> <button type="button" class="ylos">↑</button> <button type="button" class="alas">↓</button></li> <li><span>Kaakao</span> <button type="button" class="ylos">↑</button> <button type="button" class="alas">↓</button></li> </ul> <p id="ilmoitus" aria-live="polite" class="vain-ruudunlukijalle"></p> </div>
Javascript
/* suorita tämä koodi Run-painikkeella */ /* Koodikoneen Run-painike suorittaa koodin uudelleen, jolloin samat käsittelijät liitettäisiin toistamiseen. Siksi elementti korvataan kopiollaan: kopio ei peri tapahtumankäsittelijöitä. */ const puhdista = function (elementti) { const kopio = elementti.cloneNode(true); elementti.replaceWith(kopio); return kopio; }; const lista = puhdista(document.getElementById("lista")); const ilmoitus = document.getElementById("ilmoitus"); const nimi = function (kohta) { return kohta.querySelector("span").textContent; }; const jarjestysluku = function (kohta) { /* indexOf tarvitsee taulukon: children on HTMLCollection */ return [...lista.children].indexOf(kohta) + 1; }; /* Muutoksesta on kerrottava: näkevä käyttäjä huomaa sen heti, mutta ruudunlukijalle mikään ei muuttunut. */ const tiedota = function (kohta) { ilmoitus.textContent = nimi(kohta) + " siirretty kohtaan " + jarjestysluku(kohta) + "/" + lista.children.length; console.log(ilmoitus.textContent); }; /* Painikkeiden nimilappuihin kohteen nimi: ruudunlukija lukee painikkeet myös irrallaan listasta, eikä pelkkä nuoli kerro mitä siirretään. */ const paivitaNimilaput = function () { for (const kohta of lista.children) { kohta.querySelector(".ylos").setAttribute("aria-label", "Siirrä ylös: " + nimi(kohta)); kohta.querySelector(".alas").setAttribute("aria-label", "Siirrä alas: " + nimi(kohta)); } }; paivitaNimilaput(); /* ---------------------------------------- painikkeet: vaihtoehto raahaamiselle */ lista.addEventListener("click", function (e) { const nappi = e.target.closest("button"); if (nappi === null) { return; } const kohta = nappi.closest("li"); if (nappi.classList.contains("ylos") && kohta.previousElementSibling !== null) { /* before siirtää elementin: samaa solmua ei voi olla kahdessa paikassa */ kohta.previousElementSibling.before(kohta); } else if (nappi.classList.contains("alas") && kohta.nextElementSibling !== null) { kohta.nextElementSibling.after(kohta); } else { return; } paivitaNimilaput(); tiedota(kohta); nappi.focus(); }); /* ---------------------------------------- raahaus pointer-tapahtumilla */ /* HUOM. Kaksi asiaa, jotka menevät helposti vikaan: 1. Listan li-elementeillä EI ole draggable-attribuuttia. Jos olisi, selain käynnistäisi hiirellä natiivin raahauksen, joka keskeyttäisi pointer-eleen ja laukaisisi pointercancel-tapahtuman heti. Valitse siis jompikumpi tekniikka, älä molempia. 2. Kaappaus otetaan LISTALLE eikä raahattavalle kohdalle. Kaappaus on sidottu elementin paikkaan DOM-puussa: jos kaapattu elementti irrotetaan ja liitetään toiseen kohtaan, kaappaus vapautuu ja pointercancel laukeaa. Koska raahattavaa kohtaa juuri siirretään liikkeen aikana, kaappaus on otettava elementille joka pysyy paikallaan. */ let raahattava = null; lista.addEventListener("pointerdown", function (e) { /* painikkeet hoidetaan clickillä */ if (e.target.closest("button") !== null) { return; } /* monikosketuksessa vain ensisijainen osoitin */ if (!e.isPrimary) { return; } const kohta = e.target.closest("li"); if (kohta === null) { return; } /* estetään tekstin maalaaminen raahauksen aikana */ e.preventDefault(); raahattava = kohta; kohta.classList.add("raahataan"); /* kaappaus listalle, ei kohdalle: kts. huomautus yllä */ lista.setPointerCapture(e.pointerId); }); lista.addEventListener("pointermove", function (e) { if (raahattava === null) { return; } /* Raahattava on osoittimen alla, joten se siirretään hetkeksi pois tieltä ennen kuin katsotaan mikä alla on. */ raahattava.style.pointerEvents = "none"; const alla = document.elementFromPoint(e.clientX, e.clientY); raahattava.style.pointerEvents = ""; const kohde = alla === null ? null : alla.closest("#lista li"); if (kohde === null || kohde === raahattava) { return; } /* Kumpaan suuntaan siirretään? Vertaillaan järjestyslukuja. */ if (jarjestysluku(raahattava) > jarjestysluku(kohde)) { kohde.before(raahattava); } else { kohde.after(raahattava); } }); const lopeta = function (e) { if (raahattava === null) { return; } raahattava.classList.remove("raahataan"); /* pointercancel tarkoittaa keskeytystä: ele ei päättynyt pudotukseen */ if (e.type === "pointercancel") { console.log("pointercancel: ele keskeytyi"); } else { paivitaNimilaput(); tiedota(raahattava); } raahattava = null; }; lista.addEventListener("pointerup", lopeta); lista.addEventListener("pointercancel", lopeta);
Konsoli