mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
1. note_intention: Delete old file only when a genuinely new upload arrives
(32-char hex file_id), not when the FilePond pool preserves an existing
file by sending its DB integer ID. Previously the DB integer ID
triggered $hasNewNote=true, which deleted the existing note_intention
from disk+DB, then handleFilePondSingleFile couldn't re-process it
because the regex requires a hex pattern. Same fix applied to cover.
2. All file deletions now use deleteThesisFileToTrash() which renames
files to tmp/_trash/ instead of unlinking. The trash preserves
original filenames prefixed with DB id for traceability. Skips
website URLs and PeerTube refs (no disk file).
3. Storage prefix changed from theses/ to documents/ to reflect that
the folder holds all document types (determined by file_type in DB).
MediaController visibility gate supports both prefixes for backward
compat with existing files.
4. File browser + relink feature for orphaned files:
- /admin/fragments/file-browser.php — HTMX tree browser for
storage/documents/ and storage/theses/
- /admin/actions/filepond/relink.php — POST endpoint that inserts
a thesis_files row pointing to existing on-disk file
- Per-pool "📂 Relier" buttons (edit mode only)
- JS: XamxamOpenFileBrowser / XamxamRelinkFile with FilePond integration
- CSS: .relink-modal dialog + .file-browser tree styles
141 lines
5.6 KiB
JavaScript
141 lines
5.6 KiB
JavaScript
/**
|
|
* jury-autocomplete.js — inlines autocomplete for jury member text inputs.
|
|
*
|
|
* Each jury sub-fieldset (<fieldset class="admin-jury-lecteurs">) gains a shared
|
|
* suggestion dropdown positioned below the last input. Typing in any input within
|
|
* that fieldset triggers an HTMX POST to the pill-search fragment (type=supervisor).
|
|
* Clicking a suggestion fills the input that triggered the search.
|
|
*
|
|
* Data attributes on the fieldset:
|
|
* data-jury-autocomplete — marks the fieldset for initialisation
|
|
* data-jury-hx-post — HTMX endpoint URL (required)
|
|
* data-jury-hx-target — CSS selector for the shared dropdown (optional)
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
function initAll() {
|
|
document.querySelectorAll('[data-jury-autocomplete]:not([data-jury-autocomplete-initialized])').forEach(function (fieldset) {
|
|
fieldset.setAttribute('data-jury-autocomplete-initialized', '1');
|
|
initFieldset(fieldset);
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', initAll);
|
|
document.body.addEventListener('htmx:afterSwap', initAll);
|
|
|
|
function initFieldset(fieldset) {
|
|
var hxPost = fieldset.getAttribute('data-jury-hx-post') || '/admin/fragments/pill-search.php';
|
|
var role = fieldset.getAttribute('data-jury-role') || '';
|
|
var dropdown = fieldset.querySelector('.jury-suggestions');
|
|
if (!dropdown) {
|
|
dropdown = document.createElement('div');
|
|
dropdown.className = 'jury-suggestions tag-search-suggestions';
|
|
dropdown.setAttribute('role', 'listbox');
|
|
// Insert after the list container
|
|
var list = fieldset.querySelector('.admin-jury-list');
|
|
if (list) {
|
|
list.insertAdjacentElement('afterend', dropdown);
|
|
} else {
|
|
fieldset.appendChild(dropdown);
|
|
}
|
|
}
|
|
|
|
var activeInput = null;
|
|
var selectedIdx = -1;
|
|
var debounceTimer = null;
|
|
|
|
// Click on suggestion → fill the active input
|
|
dropdown.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('.tag-search-item');
|
|
if (!btn) return;
|
|
var name = (btn.getAttribute('data-tag-name') || '').trim();
|
|
if (!name || !activeInput) return;
|
|
activeInput.value = btn.classList.contains('tag-search-item--create')
|
|
? activeInput.value.trim()
|
|
: name;
|
|
dropdown.innerHTML = '';
|
|
selectedIdx = -1;
|
|
activeInput.focus();
|
|
});
|
|
|
|
// Highlighting helper
|
|
function highlight(idx) {
|
|
var items = dropdown.querySelectorAll('.tag-search-item');
|
|
for (var i = 0; i < items.length; i++) {
|
|
items[i].classList.toggle('tag-search-item--highlight', i === idx);
|
|
}
|
|
}
|
|
|
|
fieldset.addEventListener('input', function (e) {
|
|
var inp = e.target.closest('input[type="text"]');
|
|
if (!inp) return;
|
|
|
|
activeInput = inp;
|
|
var q = inp.value.trim();
|
|
|
|
// Build the hx-include query — include hidden type=supervisor
|
|
var typeInput = fieldset.querySelector('input[name="type"][value="supervisor"]');
|
|
var includeSelector = typeInput ? '[name="type"][value="supervisor"]' : '';
|
|
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(function () {
|
|
if (q === '') {
|
|
dropdown.innerHTML = '';
|
|
selectedIdx = -1;
|
|
return;
|
|
}
|
|
|
|
// Manual HTMX POST
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('POST', hxPost);
|
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
xhr.setRequestHeader('HX-Request', 'true');
|
|
xhr.onload = function () {
|
|
if (xhr.status === 200) {
|
|
dropdown.innerHTML = xhr.responseText;
|
|
selectedIdx = -1;
|
|
}
|
|
};
|
|
var params = 'type=supervisor&q=' + encodeURIComponent(q) + (role ? '&role=' + encodeURIComponent(role) : '');
|
|
xhr.send(params);
|
|
}, 200);
|
|
});
|
|
|
|
// Keyboard navigation
|
|
fieldset.addEventListener('keydown', function (e) {
|
|
var items = dropdown.querySelectorAll('.tag-search-item');
|
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
if (items.length === 0) return;
|
|
e.preventDefault();
|
|
if (e.key === 'ArrowDown') {
|
|
selectedIdx = (selectedIdx + 1) % items.length;
|
|
} else {
|
|
selectedIdx = selectedIdx <= 0 ? items.length - 1 : selectedIdx - 1;
|
|
}
|
|
highlight(selectedIdx);
|
|
} else if (e.key === 'Enter') {
|
|
if (items.length > 0 && dropdown.innerHTML !== '') {
|
|
e.preventDefault();
|
|
if (selectedIdx >= 0 && selectedIdx < items.length) {
|
|
items[selectedIdx].click();
|
|
} else {
|
|
items[0].click();
|
|
}
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
dropdown.innerHTML = '';
|
|
selectedIdx = -1;
|
|
}
|
|
});
|
|
|
|
// Close dropdown on outside click
|
|
document.addEventListener('click', function (e) {
|
|
if (!fieldset.contains(e.target)) {
|
|
dropdown.innerHTML = '';
|
|
selectedIdx = -1;
|
|
}
|
|
});
|
|
}
|
|
})();
|