mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-26 08:39:18 +02:00
- Remove overtype-webcomponent.min.js (zero references) - Extract copyLogContent + fallbackCopy + HTMX tab-updater → app/admin-logs.js (removes duplicate from both system.php and parametres.php) - Extract copyUrl → app/clipboard.js (shared by acces.php) - Extract tag/language pill-search logic → app/pill-search.js Generalized with data-pill-search attributes, auto-inits via DOMContentLoaded + htmx:afterSwap - Extract access-request form handler → app/access-request.js (was inline in templates/public/tfe.php) Files created: admin-logs.js, clipboard.js, pill-search.js, access-request.js Files modified: 9 templates/controllers to drop inline scripts and reference external JS files
172 lines
7.5 KiB
JavaScript
172 lines
7.5 KiB
JavaScript
/**
|
|
* pill-search.js — generalized pill-based search component for tags and languages.
|
|
*
|
|
* Initialisez avec un conteneur ayant l'attribut data-pill-search :
|
|
* <div data-pill-search data-pill-name="tag" data-pill-max="10" data-pill-min="3" data-pill-required="1">
|
|
*
|
|
* DOM attendu à l'intérieur du conteneur :
|
|
* - .tag-search-pills → conteneur des pills
|
|
* - .tag-search-input → champ de recherche (avec hx-post, hx-trigger, etc.)
|
|
* - .tag-search-suggestions → dropdown
|
|
* - .tag-search-count → compteur
|
|
* - .tag-search-counter → wrapper du compteur
|
|
* - .tag-search-input-wrap → wrapper du champ de recherche
|
|
* - .tag-search-max-msg → message "maximum atteint"
|
|
*
|
|
* Options (par attribut data) :
|
|
* data-pill-name → nom pour les inputs cachés (ex: "tag", "language_autre")
|
|
* data-pill-max → max pills (default 10)
|
|
* data-pill-min → min pills requis (default 0)
|
|
* data-pill-required → si "1", active l'affichage du minimum
|
|
* data-pill-role → "tag" (lowercase) ou "lang" (ucfirst)
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
function initAll() {
|
|
document.querySelectorAll('[data-pill-search]:not([data-pill-search-initialized])').forEach(function (container) {
|
|
container.setAttribute('data-pill-search-initialized', '1');
|
|
initPillSearch(container);
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', initAll);
|
|
document.body.addEventListener('htmx:afterSwap', initAll);
|
|
|
|
function initPillSearch(container) {
|
|
var pills = container.querySelector('.tag-search-pills');
|
|
var search = container.querySelector('.tag-search-input');
|
|
var dropdown = container.querySelector('.tag-search-suggestions');
|
|
var countEl = container.querySelector('.tag-search-count');
|
|
var counter = container.querySelector('.tag-search-counter');
|
|
var maxTags = parseInt(container.getAttribute('data-pill-max')) || 10;
|
|
var minTags = parseInt(container.getAttribute('data-pill-min')) || 0;
|
|
var required = container.getAttribute('data-pill-required') === '1';
|
|
var inputName = container.getAttribute('data-pill-name') || 'tag';
|
|
var role = container.getAttribute('data-pill-role') || 'tag';
|
|
var selectedIdx = -1;
|
|
|
|
if (!pills || !search || !dropdown) return;
|
|
|
|
function normalize(name) {
|
|
return name.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
}
|
|
|
|
function pillAlreadyExists(name) {
|
|
var norm = normalize(name);
|
|
var existing = pills.querySelectorAll('.tag-pill-name');
|
|
for (var i = 0; i < existing.length; i++) {
|
|
if (normalize(existing[i].textContent) === norm) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function updateCount() {
|
|
var n = pills.querySelectorAll('.tag-pill').length;
|
|
var suffix = required ? ' (min ' + minTags + ')' : '';
|
|
if (countEl) countEl.textContent = n + '/' + maxTags + suffix;
|
|
if (counter) counter.style.display = (n > 0 || required) ? '' : 'none';
|
|
if (countEl && required) {
|
|
countEl.style.color = n < minTags ? 'var(--text-danger)' : 'var(--accent)';
|
|
}
|
|
|
|
var wrap = container.querySelector('.tag-search-input-wrap');
|
|
var maxMsg = container.querySelector('.tag-search-max-msg');
|
|
if (n >= maxTags) {
|
|
if (wrap) wrap.style.display = 'none';
|
|
if (maxMsg) maxMsg.style.display = '';
|
|
} else {
|
|
if (wrap) { wrap.style.display = ''; if (search) search.style.display = ''; }
|
|
if (maxMsg) maxMsg.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
pills.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('.tag-pill-remove');
|
|
if (!btn) return;
|
|
var pill = btn.closest('.tag-pill');
|
|
pill.remove();
|
|
updateCount();
|
|
var wrap = container.querySelector('.tag-search-input-wrap');
|
|
var inp = container.querySelector('.tag-search-input');
|
|
if (wrap && inp) { wrap.style.display = ''; inp.style.display = ''; }
|
|
});
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
function selectPill(btn) {
|
|
var name = normalize(btn.getAttribute('data-tag-name') || '');
|
|
if (!name) return;
|
|
if (pillAlreadyExists(name)) return;
|
|
if ((pills.querySelectorAll('.tag-pill').length) >= maxTags) return;
|
|
|
|
var escaped = htmlEscape(name);
|
|
var pill = document.createElement('span');
|
|
pill.className = 'tag-pill';
|
|
pill.innerHTML = '<input type="hidden" name="' + inputName + '[]" value="' + escaped + '">'
|
|
+ '<span class="tag-pill-name">' + escaped + '</span>'
|
|
+ '<button type="button" class="tag-pill-remove" title="Retirer \u00AB\u00A0' + escaped + '\u00A0\u00BB" aria-label="Retirer ' + escaped + '">'
|
|
+ '<svg width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM112,168V104a8,8,0,0,1,16,0v64a8,8,0,0,1-16,0Zm48,0V104a8,8,0,0,1,16,0v64a8,8,0,0,1-16,0Z"></path></svg>'
|
|
+ '</button>';
|
|
pills.appendChild(pill);
|
|
updateCount();
|
|
search.value = '';
|
|
dropdown.innerHTML = '';
|
|
selectedIdx = -1;
|
|
search.focus();
|
|
}
|
|
|
|
dropdown.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('.tag-search-item');
|
|
if (!btn) return;
|
|
selectPill(btn);
|
|
});
|
|
|
|
search.addEventListener('keydown', function (e) {
|
|
var items = dropdown.querySelectorAll('.tag-search-item');
|
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (items.length === 0) return;
|
|
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) {
|
|
e.preventDefault();
|
|
if (selectedIdx >= 0 && selectedIdx < items.length) {
|
|
selectPill(items[selectedIdx]);
|
|
} else {
|
|
selectPill(items[0]);
|
|
}
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
dropdown.innerHTML = '';
|
|
selectedIdx = -1;
|
|
}
|
|
});
|
|
|
|
search.addEventListener('blur', function () {
|
|
setTimeout(function () {
|
|
if (!dropdown.contains(document.activeElement)) {
|
|
dropdown.innerHTML = '';
|
|
selectedIdx = -1;
|
|
}
|
|
}, 150);
|
|
});
|
|
|
|
function htmlEscape(str) {
|
|
var el = document.createElement('span');
|
|
el.textContent = str;
|
|
return el.innerHTML;
|
|
}
|
|
}
|
|
})();
|