mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Add language-search component for Autre Langue input + active search in lists
Mirrors the mots-clé tag-search system: dropdown suggestions from existing languages via HTMX, pill display with bin-icon remove buttons, 'Créer' option for new languages. Replaces the plain text input. - New partial: templates/partials/form/language-search.php - New fragment: public/partage/language-search-fragment.php - Admin wrapper: public/admin/language-search-fragment.php - Updated language-autre-fragment to return just the required asterisk indicator - Updated both controllers to handle language_autre as array (pill-based) with backward-compatible string path - Updated edit form to compute selectedOtherLanguages from DB - Registered new route in partage/index.php - Fix CSV importer: split comma-separated language column into individual entries - Add htmx active search to admin index, title line-clamp, predefined languages only in checkboxes - Admin index: filter form now uses htmx triggers (input delay:300ms on search, change on selects) to actively search without page reload - Sort links include hx-push-url for back-button support - Added loading indicator bar (.admin-search-indicator) - Title column: line-clamp at 2 lines with overflow hidden, native title attr tooltip for full text - Language checkboxes now show only 3 predefined languages (Français, Anglais, Néerlandais); all others go via the Autre langue search component - Added Database::getPredefinedLanguages() and excluded predefined from language-search-fragment suggestions - Included hidden sort/dir inputs in table-wrap so sort state preserved across filter changes - Fix language-search: block 'Créer' for predefined languages in dropdown The 'Créer' option in the language-search dropdown now also checks against the predefined set (français, anglais, néerlandais) to avoid offering creation of languages that already exist as checkboxes.
This commit is contained in:
@@ -56,6 +56,13 @@ if ($slug === 'tag-search-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Special route: /partage/language-search-fragment (HTMX fragment — interactive language search)
|
||||
if ($slug === 'language-search-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
App::boot();
|
||||
require_once __DIR__ . '/language-search-fragment.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Special route: /partage/recapitulatif?id=N
|
||||
if ($slug === 'recapitulatif' || $slug === 'recapitulatif.php') {
|
||||
App::boot();
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
/**
|
||||
* language-autre-fragment.php
|
||||
*
|
||||
* Shared HTMX fragment include: returns the "Autre(s) langue(s)" input row
|
||||
* when no standard language checkbox is checked, or the plain (non-required)
|
||||
* variant when at least one is checked.
|
||||
* Shared HTMX fragment include: returns the requirement asterisk for the
|
||||
* "Autre(s) langue(s)" label. When no standard language checkbox is checked,
|
||||
* an asterisk is rendered to signal the field is required (server-side validated).
|
||||
*
|
||||
* Included by:
|
||||
* - /admin/language-autre-fragment.php (AdminAuth gated)
|
||||
@@ -12,25 +12,11 @@
|
||||
*
|
||||
* Expected POST:
|
||||
* languages[] — selected language IDs (may be absent)
|
||||
* language_autre — current free-text value (for repopulation)
|
||||
*/
|
||||
|
||||
$selectedIds = isset($_POST['languages']) && is_array($_POST['languages'])
|
||||
? $_POST['languages']
|
||||
: [];
|
||||
$currentValue = htmlspecialchars(trim($_POST['language_autre'] ?? ''));
|
||||
$anyChecked = !empty($selectedIds);
|
||||
?>
|
||||
<div id="language-autre-row">
|
||||
<div>
|
||||
<label for="language_autre">Autre(s) langue(s) :<?= !$anyChecked ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||
<div>
|
||||
<input type="text"
|
||||
id="language_autre"
|
||||
name="language_autre"
|
||||
value="<?= $currentValue ?>"
|
||||
<?= !$anyChecked ? 'required' : '' ?>>
|
||||
<small>Si votre TFE contient une langue absente de la liste, précisez-la ici.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span id="language-autre-required"><?= !$anyChecked ? ' <span class="asterisk">*</span>' : '' ?></span>
|
||||
|
||||
107
app/public/partage/language-search-fragment.php
Normal file
107
app/public/partage/language-search-fragment.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/**
|
||||
* language-search-fragment.php
|
||||
*
|
||||
* Shared HTMX fragment: returns matching language suggestions for the
|
||||
* "Autre(s) langue(s)" interactive search input.
|
||||
*
|
||||
* Included by:
|
||||
* - /admin/language-search-fragment.php (AdminAuth gated)
|
||||
* - partage/index.php special route (public, session already booted)
|
||||
*
|
||||
* Expected POST:
|
||||
* q — search query string (partial language name)
|
||||
* language_autre[] — already selected language names (for exclusion)
|
||||
*/
|
||||
require_once __DIR__ . '/../../src/Database.php';
|
||||
|
||||
$query = trim(preg_replace('/\s+/', ' ', strtolower($_POST['q'] ?? '')));
|
||||
$currentLanguages = isset($_POST['language_autre']) && is_array($_POST['language_autre'])
|
||||
? array_map(function($l) { return trim(preg_replace('/\s+/', ' ', strtolower($l))); }, $_POST['language_autre'])
|
||||
: [];
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Search existing languages by name, excluding predefined ones (already shown as checkboxes)
|
||||
$predefined = ["français", "anglais", "néerlandais", "francais", "neerlandais"];
|
||||
if ($query !== '') {
|
||||
$placeholders = implode(',', array_fill(0, count($predefined), '?'));
|
||||
$stmt = $db->getConnection()->prepare(
|
||||
"SELECT l.id, UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2) as name,
|
||||
COUNT(DISTINCT tl.thesis_id) as thesis_count
|
||||
FROM languages l
|
||||
LEFT JOIN thesis_languages tl ON l.id = tl.language_id
|
||||
WHERE LOWER(l.name) LIKE ?
|
||||
AND LOWER(l.name) NOT IN ($placeholders)
|
||||
GROUP BY l.id
|
||||
ORDER BY LOWER(l.name) = ? DESC, thesis_count DESC, LOWER(l.name)
|
||||
LIMIT 10"
|
||||
);
|
||||
$stmt->execute(array_merge([$query . '%'], $predefined, [$query]));
|
||||
} else {
|
||||
$placeholders = implode(',', array_fill(0, count($predefined), '?'));
|
||||
$stmt = $db->getConnection()->prepare(
|
||||
"SELECT l.id, UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2) as name,
|
||||
COUNT(DISTINCT tl.thesis_id) as thesis_count
|
||||
FROM languages l
|
||||
LEFT JOIN thesis_languages tl ON l.id = tl.language_id
|
||||
WHERE LOWER(l.name) NOT IN ($placeholders)
|
||||
GROUP BY l.id
|
||||
ORDER BY thesis_count DESC, LOWER(l.name)
|
||||
LIMIT 10"
|
||||
);
|
||||
$stmt->execute($predefined);
|
||||
}
|
||||
|
||||
$results = $stmt->fetchAll();
|
||||
|
||||
// Deduplicate results by lowercase name
|
||||
$seen = [];
|
||||
$results = array_values(array_filter($results, function($lang) use (&$seen) {
|
||||
$key = strtolower($lang['name']);
|
||||
if (isset($seen[$key])) return false;
|
||||
$seen[$key] = true;
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Filter out already-selected languages (case-insensitive)
|
||||
$results = array_values(array_filter($results, function($lang) use ($currentLanguages) {
|
||||
return !in_array(strtolower($lang['name']), $currentLanguages, true);
|
||||
}));
|
||||
|
||||
// Check if query exactly matches an existing language (case-insensitive)
|
||||
// Also check against predefined languages to avoid suggesting creation of a checkbox language
|
||||
$exactExists = false;
|
||||
foreach ($results as $lang) {
|
||||
if (strcasecmp($lang['name'], $query) === 0) {
|
||||
$exactExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$exactExists && $query !== '') {
|
||||
$normalisedQuery = strtolower($query);
|
||||
$normalisedPredefined = array_map('strtolower', $predefined);
|
||||
if (in_array($normalisedQuery, $normalisedPredefined, true)) {
|
||||
$exactExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If no exact match and query non-empty, suggest creation
|
||||
$canCreate = ($query !== '' && !$exactExists && !in_array($query, $currentLanguages, true));
|
||||
?>
|
||||
<?php if (empty($results) && !$canCreate): ?>
|
||||
<div class="tag-search-empty">Aucune langue trouvée.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($results as $lang): ?>
|
||||
<button type="button" class="tag-search-item" data-tag-id="<?= (int)$lang['id'] ?>" data-tag-name="<?= htmlspecialchars($lang['name']) ?>">
|
||||
<span class="tag-search-item-name"><?= htmlspecialchars($lang['name']) ?></span>
|
||||
<span class="tag-search-item-count">(<?= (int)$lang['thesis_count'] ?>)</span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($canCreate): ?>
|
||||
<button type="button" class="tag-search-item tag-search-item--create" data-tag-name="<?= htmlspecialchars($query) ?>">
|
||||
<span class="tag-search-item-name">Créer « <?= htmlspecialchars($query) ?> »</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
Reference in New Issue
Block a user