/* * Gallery editor: putting images into topics. * * Two ways in, one code path: the ⋯ menu on each thumbnail, and dragging a * thumbnail onto another section. Both call admin/topics-api.php and then move * the
in place, because reloading the page after every one of a few * hundred assignments is not an editing experience. * * The move is optimistic and reverts on failure, so the page never shows an * arrangement the server did not accept. * * Deletion is deliberately not here: it stays a plain form post inside the menu, * with the same confirmation and the same server path it has always had. */ (function () { 'use strict'; var root = document.getElementById('topic-blocks'); if (!root) return; var api = root.dataset.api; var slug = root.dataset.slug; var csrf = root.dataset.csrf; /* A private drag type. The uploader's dropzone reacts to "Files"; this makes the two kinds of drag tell themselves apart, so dragging a thumbnail never looks like dropping a photo from the desktop, or the other way round. */ var DRAG_TYPE = 'application/x-gallery-image'; var status = document.createElement('p'); status.className = 'help topic-status'; status.hidden = true; root.parentNode.insertBefore(status, root); function fail(message) { status.textContent = message; status.hidden = false; } function clearStatus() { status.hidden = true; } /* ---- Server call ---------------------------------------------------- */ function assign(key, topic) { var body = new FormData(); body.append('slug', slug); body.append('action', 'assign'); body.append('key', key); body.append('topic', topic); return fetch(api, { method: 'POST', headers: { 'X-CSRF-Token': csrf }, body: body }).then(function (response) { return response.json().catch(function () { return { error: 'Server returned a non-JSON response (HTTP ' + response.status + ')' }; }).then(function (data) { if (!response.ok || !data.ok) { throw new Error(data.error || ('Assign failed (HTTP ' + response.status + ')')); } }); }); } /* ---- Moving the thumbnail ------------------------------------------- */ function blockFor(topic) { /* Topic ids are [A-Za-z0-9_-] only, so this cannot need escaping. */ return root.querySelector('.topic-block[data-topic="' + topic + '"]'); } function recount() { Array.prototype.forEach.call(root.querySelectorAll('.topic-block'), function (block) { var badge = block.querySelector('.topic-count'); if (badge) badge.textContent = String(block.querySelectorAll('figure').length); }); } /* Insert where the server will render it after the next reload: sections keep the images in their upload order, which is what data-order carries. */ function insert(figure, row) { var order = parseInt(figure.dataset.order, 10) || 0; var after = Array.prototype.filter.call(row.children, function (el) { return el !== figure && (parseInt(el.dataset.order, 10) || 0) > order; })[0]; row.insertBefore(figure, after || null); } function markCurrent(figure, topic) { Array.prototype.forEach.call(figure.querySelectorAll('.topic-pick'), function (pick) { pick.classList.toggle('is-current', pick.dataset.topic === topic); }); } /* Move the thumbnail first, put it back if the server disagrees. */ function move(figure, topic) { var block = blockFor(topic); if (!block) return; var row = block.querySelector('.thumb-row'); var from = figure.parentNode; var before = figure.nextElementSibling; if (row === from) { closeMenus(); return; } clearStatus(); insert(figure, row); markCurrent(figure, topic); recount(); assign(figure.dataset.key, topic).catch(function (err) { from.insertBefore(figure, before); markCurrent(figure, from.parentNode.dataset.topic); recount(); fail(err.message); }); closeMenus(); } /* ---- The ⋯ menu ------------------------------------------------------ */ function closeMenus(except) { Array.prototype.forEach.call(root.querySelectorAll('.thumb-menu'), function (menu) { if (menu === except) return; menu.hidden = true; menu.parentNode.querySelector('.thumb-menu-btn').setAttribute('aria-expanded', 'false'); }); } root.addEventListener('click', function (e) { var button = e.target.closest('.thumb-menu-btn'); if (button) { var menu = button.parentNode.querySelector('.thumb-menu'); var open = menu.hidden; closeMenus(menu); menu.hidden = !open; button.setAttribute('aria-expanded', open ? 'true' : 'false'); return; } var pick = e.target.closest('.topic-pick'); if (pick) { move(pick.closest('figure'), pick.dataset.topic); return; } /* A click anywhere else inside the editor, including on another thumbnail, dismisses an open menu. */ if (!e.target.closest('.thumb-menu')) closeMenus(); }); document.addEventListener('click', function (e) { if (!e.target.closest || !e.target.closest('#topic-blocks')) closeMenus(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeMenus(); }); /* ---- Drag and drop --------------------------------------------------- */ var dragged = null; root.addEventListener('dragstart', function (e) { var figure = e.target.closest('figure'); if (!figure) return; dragged = figure; figure.classList.add('is-dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData(DRAG_TYPE, figure.dataset.key); /* Some browsers refuse a drag that sets no text/plain. */ e.dataTransfer.setData('text/plain', figure.dataset.key); closeMenus(); }); root.addEventListener('dragend', function () { if (dragged) dragged.classList.remove('is-dragging'); dragged = null; Array.prototype.forEach.call(root.querySelectorAll('.drag-over'), function (block) { block.classList.remove('drag-over'); }); }); function ours(e) { var types = e.dataTransfer ? e.dataTransfer.types : null; return !!types && Array.prototype.indexOf.call(types, DRAG_TYPE) !== -1; } root.addEventListener('dragover', function (e) { if (!ours(e)) return; // a file drag belongs to the uploader, not here var block = e.target.closest('.topic-block'); if (!block) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; block.classList.add('drag-over'); }); root.addEventListener('dragleave', function (e) { var block = e.target.closest('.topic-block'); /* Ignore the leave events fired while crossing the section's own children, or the highlight would flicker across every thumbnail. */ if (block && !block.contains(e.relatedTarget)) block.classList.remove('drag-over'); }); root.addEventListener('drop', function (e) { if (!ours(e)) return; var block = e.target.closest('.topic-block'); if (!block) return; e.preventDefault(); block.classList.remove('drag-over'); if (dragged) move(dragged, block.dataset.topic); }); })();