/* Makenotwork — Collections Picker */ 'use strict'; /** * Open a collection picker dropdown anchored to the given element. * Works on item page, discover cards, and library rows. * * @param {string} itemId - The item UUID to add/remove from collections. * @param {HTMLElement} anchor - The element to anchor the picker to. * @param {object} [opts] - Options. * @param {string} [opts.position] - 'below' (default) or 'above'. */ function openCollectionPicker(itemId, anchor, opts) { opts = opts || {}; // Close any existing picker closeCollectionPicker(); var wrapper = anchor.closest('.collection-picker-anchor') || anchor.parentElement; wrapper.style.position = 'relative'; var picker = document.createElement('div'); picker.id = 'collection-picker-active'; picker.className = 'collection-picker'; if (opts.position === 'above') { picker.style.bottom = '100%'; picker.style.top = 'auto'; } picker.innerHTML = '
Loading...
' + '
' + '
' + '' + '' + '
'; wrapper.appendChild(picker); picker.dataset.itemId = itemId; // Attach submit handler via addEventListener (avoids inline handler XSS) var form = picker.querySelector('.collection-picker-create form'); form.addEventListener('submit', function(e) { e.preventDefault(); collectionPickerCreate(itemId, form); }); // Load collections collectionPickerLoad(itemId, picker); // Close on outside click (deferred so this click doesn't close it) setTimeout(function() { document.addEventListener('click', collectionPickerOutsideClick); }, 0); } function closeCollectionPicker() { var existing = document.getElementById('collection-picker-active'); if (existing) existing.remove(); document.removeEventListener('click', collectionPickerOutsideClick); } function collectionPickerOutsideClick(e) { var picker = document.getElementById('collection-picker-active'); if (picker && !picker.contains(e.target)) { // Check if the click target is a save button (don't close if re-clicking the trigger) if (e.target.closest('[data-collection-trigger]')) return; closeCollectionPicker(); } } function collectionPickerLoad(itemId, picker) { var list = picker.querySelector('.collection-picker-list'); fetch('/api/collections/for-item/' + itemId, { headers: csrfHeaders() }) .then(function(r) { if (r.status === 401) { list.innerHTML = '
Sign in to save items to collections.
'; picker.querySelector('.collection-picker-create').style.display = 'none'; return null; } return r.json(); }) .then(function(cols) { if (cols === null) return; if (cols.length === 0) { list.innerHTML = '
No collections yet. Create one below.
'; return; } var html = ''; for (var i = 0; i < cols.length; i++) { var c = cols[i]; html += ''; } list.innerHTML = html; }) .catch(function() { list.innerHTML = '
Failed to load collections.
'; }); } function collectionPickerToggle(collectionId, itemId, add) { fetch('/api/collections/' + collectionId + '/items/' + itemId, { method: add ? 'POST' : 'DELETE', headers: csrfHeaders() }).then(function(r) { if (r.ok) { showToast(add ? 'Added to collection' : 'Removed from collection', 'info'); collectionPickerUpdateButtons(itemId); } else { apiErrorMessage(r, add ? 'Failed to add to collection' : 'Failed to remove from collection') .then(function(m) { showToast(m, 'error'); }); } }).catch(function() { showToast(add ? 'Failed to add to collection' : 'Failed to remove from collection', 'error'); }); } function collectionPickerCreate(itemId, form) { var title = form.title.value.trim(); if (!title) return; var slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); var btn = form.querySelector('button'); btn.disabled = true; fetch('/api/collections', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ title: title, slug: slug, description: '', is_public: false }) }) .then(function(r) { if (!r.ok) return apiErrorMessage(r, 'Failed to create collection').then(function(m) { throw new Error(m); }); return r.json(); }) .then(function(col) { return fetch('/api/collections/' + col.id + '/items/' + itemId, { method: 'POST', headers: csrfHeaders() }).then(function(r2) { if (!r2.ok) return apiErrorMessage(r2, 'Failed to add item to collection').then(function(m) { throw new Error(m); }); }); }) .then(function() { form.title.value = ''; btn.disabled = false; showToast('Created collection and added item', 'info'); var picker = document.getElementById('collection-picker-active'); if (picker) collectionPickerLoad(itemId, picker); collectionPickerUpdateButtons(itemId); }) .catch(function(err) { btn.disabled = false; showToast(err.message || 'Failed to create collection', 'error'); }); } /** * After add/remove, refresh the saved state of any save buttons for this item. * Buttons use data-item-id to identify which item they belong to. */ function collectionPickerUpdateButtons(itemId) { fetch('/api/collections/for-item/' + itemId, { headers: csrfHeaders() }) .then(function(r) { return r.json(); }) .then(function(cols) { var savedCount = 0; for (var i = 0; i < cols.length; i++) { if (cols[i].in_collection) savedCount++; } var buttons = document.querySelectorAll('[data-collection-trigger][data-item-id="' + itemId + '"]'); for (var j = 0; j < buttons.length; j++) { var btn = buttons[j]; if (btn.dataset.collectionLabel) { // Full label button (item page style) btn.textContent = savedCount > 0 ? 'Saved (' + savedCount + ')' : 'Save to collection'; btn.classList.toggle('saved', savedCount > 0); } } }); } function collectionEscapeHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }