| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- /*
- * 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 <figure> 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);
- });
- })();
|