Foto's uploaden en ordenen Nieuw
Fotogalerij voor een advertentie of aanvraag: foto's toevoegen met de knop of door te slepen (direct zichtbaar via FileReader), de volgorde aanpassen door te slepen of met het toetsenbord (Spatie oppakken, pijltjes verplaatsen, Spatie neerzetten, Esc annuleren), een hoofdfoto kiezen en verwijderen met ongedaan maken. Fouten bij geen foto, te groot of te veel. Elke verplaatsing wordt voorgelezen. Animeert met View Transitions waar die er zijn; anders springt de volgorde direct om.
Foto's van je woning
De eerste foto is de hoofdfoto. Versleep met de greep of gebruik Spatie en de pijltjes.
Zo gebruik je het
- Plak de HTML op de plek waar het onderdeel moet komen.
- Zet de CSS in je stylesheet. Alle klassen hebben een eigen voorvoegsel, dus niets botst met je eigen opmaak.
- Voeg de JavaScript toe en roep init() aan met het element waar de HTML in staat.
<!-- Foto's uploaden en ordenen · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="ugh">
<div class="ugh__head">
<h3 class="ugh__h">Foto's van je woning</h3>
<span class="ugh__n" aria-live="polite"></span>
</div>
<p class="ugh__help" id="ugh-help">De eerste foto is de hoofdfoto. Versleep met de greep of gebruik Spatie en de pijltjes.</p>
<ul class="ugh__grid" role="list"></ul>
<p class="ugh__err" role="alert"></p>
<div class="ugh__undo" hidden><span></span><button type="button">Ongedaan maken</button></div>
<input type="file" class="ugh__file" id="ugh-file" accept="image/jpeg,image/png,image/webp" multiple hidden>
<p class="ugh__sr" aria-live="assertive"></p>
</div>
</div>/* Foto's uploaden en ordenen · NedDev · eigen werk. */
.ugh{--ink:#0a0a0a;--pop:#F5480F;--pd:#b83a0b;--mut:#5b5b5b;--line:rgba(10,10,10,.14);position:absolute;inset:0;overflow:auto;padding:10px 12px;font-family:'Switzer',system-ui,sans-serif;font-size:12.5px;line-height:1.35;color:var(--ink);background:#fff;scrollbar-width:thin;text-align:left}
.ugh *,.ugh *::before,.ugh *::after{box-sizing:border-box}
.ugh svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.ugh__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);margin:0}
.ugh__head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
.ugh__h{margin:0;font-size:14px;font-weight:600}
.ugh__n{font-size:11.5px;color:var(--mut);font-variant-numeric:tabular-nums}
.ugh__help{margin:2px 0 8px;font-size:11.5px;color:var(--mut)}
.ugh__grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(86px,1fr));gap:8px}
.ugh__it{position:relative;display:flex;flex-direction:column;border:1px solid var(--line);border-radius:4px;background:#fff;overflow:hidden;transition:border-color .15s,opacity .15s}
.ugh__it.is-main{border-color:var(--ink)}
.ugh__it.is-drag{opacity:.45;outline:2px dashed var(--ink);outline-offset:1px}
.ugh__it.is-grab{outline:2px solid var(--pop);outline-offset:1px}
.ugh__img{display:block;width:100%;aspect-ratio:4/3;object-fit:cover;background:#f4f3f1}
.ugh__main{position:absolute;left:4px;top:4px;padding:1px 5px;border-radius:4px;background:var(--ink);color:#fff;font-size:11px;font-weight:600}
.ugh__bar{display:flex;align-items:center;justify-content:space-between;padding:2px}
.ugh__b{display:grid;place-items:center;width:26px;height:26px;padding:0;border:0;border-radius:4px;background:none;color:var(--ink);cursor:pointer;transition:background-color .15s}
.ugh__b:hover{background:#f4f3f1}
.ugh__b:disabled{color:#b5b2ad;cursor:default}
.ugh__b:disabled:hover{background:none}
.ugh__grip{cursor:grab;touch-action:none}
.ugh__grip:active{cursor:grabbing}
.ugh__b[aria-pressed="true"]{background:var(--ink);color:#fff}
.ugh__add{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;min-height:98px;width:100%;height:100%;padding:8px;border:1px dashed rgba(10,10,10,.4);border-radius:4px;background:#f4f3f1;color:var(--ink);font:500 12px/1.25 'Switzer',system-ui,sans-serif;text-align:center;cursor:pointer;transition:border-color .15s,background-color .15s}
.ugh__add:hover,.ugh.is-over .ugh__add{border-color:var(--ink);background:#ecebe8}
.ugh__add small{font-size:11px;color:var(--mut)}
.ugh__add svg{width:18px;height:18px}
.ugh__empty{grid-column:1/-1}
.ugh__empty .ugh__add{min-height:110px}
.ugh__load{display:grid;place-items:center;aspect-ratio:4/3;background:linear-gradient(90deg,#f4f3f1,#ecebe8,#f4f3f1) 0 0/200% 100%;animation:ugh-sh 1.2s linear infinite;font-size:11px;color:var(--mut)}
@keyframes ugh-sh{to{background-position:-200% 0}}
.ugh__err{margin:8px 0 0;color:var(--pd);font-size:11.5px;font-weight:500}
.ugh__err:empty{display:none}
.ugh__undo{display:flex;align-items:center;gap:10px;margin-top:8px;padding:6px 6px 6px 10px;border-radius:4px;background:var(--ink);color:#fff;font-size:12px}
.ugh__undo[hidden]{display:none}
.ugh__undo span{flex:1;min-width:0}
.ugh__undo button{height:24px;padding:0 8px;border:1px solid rgba(255,255,255,.6);border-radius:4px;background:none;color:#fff;font:600 11.5px/1 'Switzer',system-ui,sans-serif;cursor:pointer}
.ugh__undo button:hover{background:#fff;color:var(--ink)}
.ugh button:focus-visible,.ugh__add:focus-visible{outline:2px solid var(--pop);outline-offset:1px}
@media (prefers-reduced-motion:reduce){.ugh *{transition:none!important}.ugh__load{animation:none}}// Foto's uploaden en ordenen · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
const w = root.querySelector('.ugh'); if (!w) return;
const grid = w.querySelector('.ugh__grid'), file = w.querySelector('.ugh__file'), err = w.querySelector('.ugh__err');
const count = w.querySelector('.ugh__n'), sr = w.querySelector('.ugh__sr'), undoBar = w.querySelector('.ugh__undo');
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const MAX = 8, MB = 10;
const src = n => 'https://neddev.com/lab-media/uploadgalerij-herordenen-' + n + '.webp';
let P = [{ id: 'a', src: src(4), alt: 'Voorgevel' }, { id: 'b', src: src(1), alt: 'Woonkamer' }, { id: 'c', src: src(2), alt: 'Keuken' }, { id: 'd', src: src(3), alt: 'Achtertuin' }];
let loading = 0, grab = null, undo = null, undoT = 0, uid = 0;
const I = {
grip: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>',
main: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 11 12 4l8 7v9h-5v-6H9v6H4z"/></svg>',
del: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13"/></svg>',
add: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>'
};
const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
const say = t => { sr.textContent = ''; setTimeout(() => { sr.textContent = t; }, 20); };
const vt = fn => { if (document.startViewTransition && !reduce) document.startViewTransition(fn); else fn(); };
function render(focusSel) {
count.textContent = P.length + ' van ' + MAX;
let h = P.map((p, i) => '<li class="ugh__it' + (i === 0 ? ' is-main' : '') + (grab && grab.id === p.id ? ' is-grab' : '') + '" data-id="' + p.id + '" style="view-transition-name:ugh-' + p.id + '">' +
'<img class="ugh__img" src="' + p.src + '" alt="' + esc(p.alt) + '" draggable="false">' + (i === 0 ? '<span class="ugh__main">Hoofdfoto</span>' : '') +
'<div class="ugh__bar"><button type="button" class="ugh__b ugh__grip" aria-describedby="ugh-help" aria-pressed="' + !!(grab && grab.id === p.id) + '" aria-label="Verplaats ' + esc(p.alt) + ', positie ' + (i + 1) + ' van ' + P.length + '" title="Verplaatsen">' + I.grip + '</button>' +
'<button type="button" class="ugh__b ugh__mk" aria-label="Maak ' + esc(p.alt) + ' de hoofdfoto" title="Hoofdfoto"' + (i === 0 ? ' disabled' : '') + '>' + I.main + '</button>' +
'<button type="button" class="ugh__b ugh__del" aria-label="Verwijder ' + esc(p.alt) + '" title="Verwijderen">' + I.del + '</button></div></li>').join('');
for (let k = 0; k < loading; k++) h += '<li class="ugh__it"><div class="ugh__load">Bezig met laden</div></li>';
const addBtn = '<label class="ugh__add" for="ugh-file" tabindex="0" role="button">' + I.add + '<span>' + (P.length ? 'Foto toevoegen' : 'Nog geen foto\'s. Voeg er minimaal 1 toe') + '</span><small>JPG, PNG of WebP, max ' + MB + ' MB</small></label>';
if (P.length + loading < MAX) h += P.length ? '<li>' + addBtn + '</li>' : '<li class="ugh__empty">' + addBtn + '</li>';
grid.innerHTML = h;
if (focusSel) { const f = grid.querySelector(focusSel); if (f) f.focus(); }
}
function move(id, to) {
const from = P.findIndex(p => p.id === id); if (from < 0 || to < 0 || to >= P.length || to === from) return false;
const [x] = P.splice(from, 1); P.splice(to, 0, x); return true;
}
function del(id) {
const i = P.findIndex(p => p.id === id); if (i < 0) return;
const [x] = P.splice(i, 1); undo = { x, i };
vt(() => {
render();
const next = grid.querySelectorAll('.ugh__grip')[Math.min(i, P.length - 1)];
(next || grid.querySelector('.ugh__add')).focus();
});
undoBar.querySelector('span').textContent = '“' + x.alt + '” verwijderd.'; undoBar.hidden = false;
say(x.alt + ' verwijderd. Ongedaan maken staat onder de foto\'s.');
clearTimeout(undoT); undoT = setTimeout(() => { undoBar.hidden = true; undo = null; }, 8000);
}
undoBar.querySelector('button').addEventListener('click', () => {
if (!undo) return; P.splice(undo.i, 0, undo.x); const id = undo.x.id; say(undo.x.alt + ' teruggezet op positie ' + (undo.i + 1));
undo = null; undoBar.hidden = true; clearTimeout(undoT);
vt(() => render('[data-id="' + id + '"] .ugh__grip'));
});
grid.addEventListener('click', e => {
const li = e.target.closest('.ugh__it'); if (!li || !li.dataset.id) return;
const id = li.dataset.id, p = P.find(x => x.id === id);
if (e.target.closest('.ugh__del')) { del(id); return; }
if (e.target.closest('.ugh__mk')) { move(id, 0); vt(() => render('[data-id="' + id + '"] .ugh__grip')); say(p.alt + ' is nu de hoofdfoto'); return; }
if (e.target.closest('.ugh__grip') && e.detail === 0) toggleGrab(id);
});
function toggleGrab(id) {
const p = P.find(x => x.id === id), i = P.indexOf(p);
if (grab && grab.id === id) { grab = null; render('[data-id="' + id + '"] .ugh__grip'); say(p.alt + ' neergezet op positie ' + (i + 1) + ' van ' + P.length); }
else { grab = { id, start: i }; render('[data-id="' + id + '"] .ugh__grip'); say(p.alt + ' opgepakt, positie ' + (i + 1) + ' van ' + P.length + '. Gebruik de pijltjes, Spatie om neer te zetten, Esc om te annuleren.'); }
}
grid.addEventListener('keydown', e => {
const g = e.target.closest('.ugh__grip'); if (!g) return;
const id = g.closest('.ugh__it').dataset.id, p = P.find(x => x.id === id);
if (!grab || grab.id !== id) return;
const i = P.indexOf(p);
if (/Arrow(Left|Up|Right|Down)/.test(e.key)) {
e.preventDefault();
const to = i + (/Left|Up/.test(e.key) ? -1 : 1);
if (move(id, to)) { vt(() => render('[data-id="' + id + '"] .ugh__grip')); say('Positie ' + (to + 1) + ' van ' + P.length + (to === 0 ? ', hoofdfoto' : '')); }
} else if (e.key === 'Escape') {
e.preventDefault(); move(id, grab.start); const s = grab.start; grab = null; vt(() => render('[data-id="' + id + '"] .ugh__grip')); say('Verplaatsen geannuleerd, terug op positie ' + (s + 1));
}
});
grid.addEventListener('focusout', e => {
if (!grab || !e.target.closest('.ugh__grip')) return;
const li = e.target.closest('.ugh__it');
if (li && li.dataset.id === grab.id && !(e.relatedTarget && li.contains(e.relatedTarget) && e.relatedTarget.classList.contains('ugh__grip'))) {
if (!document.hasFocus() || !e.relatedTarget) return;
grab = null; li.classList.remove('is-grab'); e.target.setAttribute('aria-pressed', 'false');
}
});
// Slepen met muis of vinger: de rij wordt live omgezet, bij loslaten opgeslagen.
let drag = null;
grid.addEventListener('pointerdown', e => {
const g = e.target.closest('.ugh__grip'); if (!g || e.button > 0) return;
e.preventDefault();
const li = g.closest('.ugh__it'); drag = { id: li.dataset.id, li, moved: false, x: e.clientX, y: e.clientY };
});
document.addEventListener('pointermove', e => {
if (!drag) return;
if (!drag.moved && Math.hypot(e.clientX - drag.x, e.clientY - drag.y) < 5) return;
drag.moved = true; drag.li.classList.add('is-drag');
const over = document.elementFromPoint(e.clientX, e.clientY), li = over && over.closest('.ugh__it');
if (!li || li === drag.li || !li.dataset.id || li.parentNode !== grid) return;
const items = [...grid.querySelectorAll('.ugh__it[data-id]')];
const a = items.indexOf(drag.li), b = items.indexOf(li);
grid.insertBefore(drag.li, a < b ? li.nextSibling : li);
});
document.addEventListener('pointerup', () => {
if (!drag) return;
const d = drag; drag = null;
if (!d.moved) { const g = d.li.querySelector('.ugh__grip'); if (g) g.focus(); return; }
const order = [...grid.querySelectorAll('.ugh__it[data-id]')].map(li => li.dataset.id);
P = order.map(id => P.find(p => p.id === id));
const i = order.indexOf(d.id); render('[data-id="' + d.id + '"] .ugh__grip');
say(P[i].alt + ' verplaatst naar positie ' + (i + 1) + (i === 0 ? ', nu de hoofdfoto' : ''));
});
// Toevoegen
function addFiles(list) {
err.textContent = '';
const msgs = [];
[...list].forEach(f => {
if (!/^image\/(jpeg|png|webp)$/.test(f.type)) { msgs.push(f.name + ' is geen foto (JPG, PNG of WebP).'); return; }
if (f.size > MB * 1048576) { msgs.push(f.name + ' is groter dan ' + MB + ' MB.'); return; }
if (P.length + loading >= MAX) { msgs.push('Maximaal ' + MAX + ' foto\'s; ' + f.name + ' is niet toegevoegd.'); return; }
loading++;
const r = new FileReader();
r.onload = () => { loading--; P.push({ id: 'u' + (++uid), src: r.result, alt: f.name.replace(/\.[^.]+$/, '') }); render(); say(f.name + ' toegevoegd, positie ' + P.length); };
r.onerror = () => { loading--; err.textContent = f.name + ' kon niet worden gelezen. Probeer het opnieuw.'; render(); };
r.readAsDataURL(f);
});
render();
if (msgs.length) err.textContent = msgs.join(' ');
}
file.addEventListener('change', () => { addFiles(file.files); file.value = ''; });
grid.addEventListener('keydown', e => { const a = e.target.closest('.ugh__add'); if (a && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); file.click(); } });
w.addEventListener('dragover', e => { if (e.dataTransfer && [...e.dataTransfer.types].includes('Files')) { e.preventDefault(); w.classList.add('is-over'); } });
w.addEventListener('dragleave', e => { if (!w.contains(e.relatedTarget)) w.classList.remove('is-over'); });
w.addEventListener('drop', e => { e.preventDefault(); w.classList.remove('is-over'); if (e.dataTransfer && e.dataTransfer.files.length) addFiles(e.dataTransfer.files); });
render();
}
init(document.querySelector('.jouw-container'));
Meer uit formulieren & invoer
Iets gezien dat je mooi vindt?
Zet het in je selectie en stuur hem op. Dan bouwen we je site eromheen, in jouw huisstijl.
Neem contact op →