Files
xamxam/app/public/assets/js/app/pill-search.js
Pontoporeia b56d073210 refactor: extract inline JS into app/ modules, remove dead overtype-webcomponent
- 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
2026-05-19 00:08:06 +02:00

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;
}
}
})();