Autosave: 20260428-225104

This commit is contained in:
Flatlogic Bot 2026-04-28 22:51:04 +00:00
parent a4cb3a5abc
commit a1559c3861

View File

@ -46,6 +46,74 @@ function scitemcustom_display_value($value): string
return $formatted === '' ? '0' : $formatted;
}
function scitemcustom_escape_like(string $value): string
{
return strtr($value, [
'\\' => '\\\\',
'%' => '\\%',
'_' => '\\_',
]);
}
function scitemcustom_search_available_items(PDO $db, int $ownerAuthId, string $query, int $limit = 15): array
{
$query = trim($query);
if ($query === '') {
return [];
}
$escapedQuery = scitemcustom_escape_like($query);
$exact = $escapedQuery;
$prefix = $escapedQuery . '%';
$contains = '%' . $escapedQuery . '%';
$limit = max(1, min(25, $limit));
$sql = "SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_uuid, cl_scobjs_type, cl_scobjs_subtype
FROM tbl_scobjs
WHERE cl_scobjs_id NOT IN (
SELECT cl_scitemcustom_obj_id
FROM tbl_scitemcustom
WHERE cl_scitemcustom_owner_auth_id = :owner_auth_id
)
AND (
cl_scobjs_name LIKE :contains_name
OR cl_scobjs_type LIKE :contains_type
OR cl_scobjs_subtype LIKE :contains_subtype
OR cl_scobjs_uuid LIKE :contains_uuid
)
ORDER BY
CASE
WHEN cl_scobjs_name = :exact_name THEN 0
WHEN cl_scobjs_name LIKE :prefix_name THEN 1
WHEN cl_scobjs_uuid = :exact_uuid THEN 2
WHEN cl_scobjs_uuid LIKE :prefix_uuid THEN 3
WHEN cl_scobjs_type LIKE :prefix_type THEN 4
WHEN cl_scobjs_subtype LIKE :prefix_subtype THEN 5
ELSE 6
END ASC,
CHAR_LENGTH(cl_scobjs_name) ASC,
cl_scobjs_name ASC,
cl_scobjs_id ASC
LIMIT {$limit}";
$stmt = $db->prepare($sql);
$stmt->execute([
'owner_auth_id' => $ownerAuthId,
'contains_name' => $contains,
'contains_type' => $contains,
'contains_subtype' => $contains,
'contains_uuid' => $contains,
'exact_name' => $exact,
'prefix_name' => $prefix,
'exact_uuid' => $exact,
'prefix_uuid' => $prefix,
'prefix_type' => $prefix,
'prefix_subtype' => $prefix,
]);
return $stmt->fetchAll() ?: [];
}
function scitemcustom_preview(string $sign, $value, string $unit): string
{
$prefix = $sign === '-' ? '-' : ($sign === '+' ? '+' : '');
@ -300,28 +368,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
$search = trim($_GET['search'] ?? '');
$search_results = [];
if ($search !== '') {
$stmt_search = $db->prepare(
"SELECT *
FROM tbl_scobjs
WHERE (cl_scobjs_name LIKE :search OR cl_scobjs_type LIKE :search OR cl_scobjs_subtype LIKE :search OR cl_scobjs_uuid LIKE :search)
AND cl_scobjs_id NOT IN (
SELECT cl_scitemcustom_obj_id
FROM tbl_scitemcustom
WHERE cl_scitemcustom_owner_auth_id = :owner_auth_id
)
ORDER BY cl_scobjs_name ASC
LIMIT 15"
);
$stmt_search->execute([
'search' => '%' . $search . '%',
'owner_auth_id' => $current_owner_auth_id,
]);
$search_results = $stmt_search->fetchAll();
if ($_SERVER['REQUEST_METHOD'] === 'GET' && (string) ($_GET['ajax'] ?? '') === 'item_suggestions') {
header('Content-Type: application/json; charset=UTF-8');
$query = trim((string) ($_GET['q'] ?? ''));
$items = [];
if ($query !== '') {
$items = array_map(static function (array $row): array {
return [
'id' => (int) $row['cl_scobjs_id'],
'name' => (string) $row['cl_scobjs_name'],
'uuid' => (string) $row['cl_scobjs_uuid'],
'type' => (string) $row['cl_scobjs_type'],
'subtype' => (string) ($row['cl_scobjs_subtype'] ?? ''),
];
}, scitemcustom_search_available_items($db, $current_owner_auth_id, $query, 12));
}
echo json_encode(['items' => $items], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
$search = trim($_GET['search'] ?? '');
$search_results = $search !== ''
? scitemcustom_search_available_items($db, $current_owner_auth_id, $search, 15)
: [];
$stmt_stats_catalog = $db->query('SELECT * FROM tbl_scstatsitem ORDER BY cl_scstatsitem_name ASC, cl_scstatsitem_id ASC');
$stats_catalog = $stmt_stats_catalog->fetchAll();
$stats_by_id = [];
@ -597,6 +670,105 @@ $current_session_user = $_SESSION['user'] ?? '';
gap: 1rem;
}
.item-picker-form {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.item-picker-wrap {
position: relative;
}
.item-picker-input {
padding-right: 3rem;
}
.item-picker-dropdown {
position: absolute;
top: calc(100% + 0.45rem);
left: 0;
right: 0;
z-index: 20;
display: flex;
flex-direction: column;
gap: 0.65rem;
max-height: 360px;
overflow-y: auto;
padding: 0.75rem;
border-radius: 12px;
border: 1px solid rgba(162, 155, 120, 0.22);
background: rgba(17, 24, 39, 0.96);
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.42);
backdrop-filter: blur(12px);
}
.item-picker-option {
display: flex;
gap: 0.85rem;
align-items: flex-start;
width: 100%;
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 10px;
padding: 0.8rem;
background: rgba(255, 255, 255, 0.035);
color: inherit;
text-align: left;
cursor: pointer;
transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}
.item-picker-option:hover,
.item-picker-option:focus-visible,
.item-picker-option.is-active {
outline: none;
border-color: rgba(162, 155, 120, 0.55);
background: rgba(162, 155, 120, 0.09);
transform: translateY(-1px);
}
.item-picker-option-empty {
cursor: default;
}
.item-picker-option-empty:hover,
.item-picker-option-empty:focus-visible {
transform: none;
border-color: rgba(255, 255, 255, 0.07);
background: rgba(255, 255, 255, 0.035);
}
.item-picker-selection {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.9rem;
border: 1px solid rgba(162, 155, 120, 0.22);
border-radius: 12px;
padding: 0.85rem 1rem;
background: rgba(162, 155, 120, 0.06);
}
.item-picker-selection-main {
display: flex;
align-items: flex-start;
gap: 0.9rem;
min-width: 0;
flex: 1;
}
.item-picker-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.item-picker-actions .btn-modern[disabled] {
opacity: 0.55;
cursor: not-allowed;
pointer-events: none;
}
.search-item,
.custom-item-card {
border: 1px solid var(--border-glow);
@ -908,6 +1080,20 @@ $current_session_user = $_SESSION['user'] ?? '';
justify-content: center;
}
.item-picker-selection {
flex-direction: column;
align-items: stretch;
}
.item-picker-actions {
flex-direction: column;
}
.item-picker-actions .btn-modern {
width: 100%;
justify-content: center;
}
.actions-row {
justify-content: stretch;
}
@ -956,14 +1142,57 @@ $current_session_user = $_SESSION['user'] ?? '';
<section class="glass-card">
<h2>Ajouter un objet</h2>
<p class="count-note">Choisis un objet depuis la base, puis attribue-lui autant de stats que nécessaire.</p>
<form method="get" action="scitemcustom.php" style="display:flex; gap:0.75rem; flex-wrap:wrap;">
<input type="text" name="search" class="form-control" value="<?php echo htmlspecialchars($search, ENT_QUOTES, 'UTF-8'); ?>" placeholder="Rechercher un objet..." style="flex:1; min-width:220px;">
<button type="submit" class="btn-modern">Rechercher</button>
<a href="scitemcustom.php" class="btn-modern danger">Reset</a>
<form method="post" action="scitemcustom.php" id="item-add-form" class="item-picker-form" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="add_custom_item">
<input type="hidden" name="obj_id" id="item-add-selected-id" value="">
<div class="item-picker-wrap">
<input
type="search"
id="item-add-search"
class="form-control item-picker-input"
placeholder="Rechercher un objet à ajouter..."
aria-label="Rechercher un objet à ajouter"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="item-add-suggestions"
spellcheck="false"
>
<div class="item-picker-dropdown hidden-by-filter" id="item-add-suggestions" role="listbox" aria-label="Suggestions d'objets"></div>
</div>
<div class="form-help" id="item-add-help">Commence à taper le nom d'un objet. La liste juste en dessous se met à jour dynamiquement et tu peux cliquer sur l'objet voulu.</div>
<div class="item-picker-selection hidden-by-filter" id="item-add-selection" aria-live="polite">
<div class="item-picker-selection-main">
<img src="" class="item-preview hidden-by-filter" id="item-add-selection-image" alt="">
<div class="search-item-content">
<strong class="item-name" id="item-add-selection-name"></strong>
<div class="item-submeta" id="item-add-selection-meta"></div>
</div>
</div>
<button type="button" class="btn-modern danger" id="item-add-clear">Effacer</button>
</div>
<div class="item-picker-actions">
<button type="submit" class="btn-modern" id="item-add-submit" disabled>Ajouter</button>
<button type="button" class="btn-modern danger" id="item-add-reset">Vider la recherche</button>
</div>
</form>
<noscript>
<div class="form-help" style="margin-top:1rem; margin-bottom:0.75rem;">JavaScript est désactivé : utilise la recherche classique ci-dessous.</div>
<form method="get" action="scitemcustom.php" style="display:flex; gap:0.75rem; flex-wrap:wrap;">
<input type="text" name="search" class="form-control" value="<?php echo htmlspecialchars($search, ENT_QUOTES, 'UTF-8'); ?>" placeholder="Rechercher un objet..." style="flex:1; min-width:220px;">
<button type="submit" class="btn-modern">Rechercher</button>
<a href="scitemcustom.php" class="btn-modern danger">Reset</a>
</form>
</noscript>
<?php if ($search !== ''): ?>
<div class="form-help" style="margin-top:1rem; margin-bottom:1rem;">Résultats disponibles pour l'ajout dans la liste Item Custom.</div>
<div class="form-help" style="margin-top:1rem; margin-bottom:1rem;">Résultats de la recherche classique pour l'ajout dans la liste Item Custom.</div>
<div class="search-result-list">
<?php if (empty($search_results)): ?>
<div class="search-item">
@ -1215,9 +1444,20 @@ $current_session_user = $_SESSION['user'] ?? '';
var emptyState = document.getElementById('item-custom-no-results');
var itemCards = Array.prototype.slice.call(document.querySelectorAll('#item-custom-list .custom-item-card'));
if (itemCards.length === 0) {
return;
}
var addForm = document.getElementById('item-add-form');
var addSearchInput = document.getElementById('item-add-search');
var addSelectedIdInput = document.getElementById('item-add-selected-id');
var addDropdown = document.getElementById('item-add-suggestions');
var addSelection = document.getElementById('item-add-selection');
var addSelectionImage = document.getElementById('item-add-selection-image');
var addSelectionName = document.getElementById('item-add-selection-name');
var addSelectionMeta = document.getElementById('item-add-selection-meta');
var addSubmit = document.getElementById('item-add-submit');
var addReset = document.getElementById('item-add-reset');
var addClear = document.getElementById('item-add-clear');
var pickerRequestTimer = null;
var pickerRequestToken = 0;
var selectedItem = null;
function normalizeValue(value) {
return (value || '')
@ -1228,6 +1468,240 @@ $current_session_user = $_SESSION['user'] ?? '';
.trim();
}
function escapeHtml(value) {
return (value || '')
.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function setPickerExpanded(isExpanded) {
if (addSearchInput) {
addSearchInput.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
}
}
function hidePickerDropdown() {
if (!addDropdown) {
return;
}
addDropdown.innerHTML = '';
addDropdown.classList.add('hidden-by-filter');
setPickerExpanded(false);
}
function showPickerDropdown() {
if (!addDropdown) {
return;
}
addDropdown.classList.remove('hidden-by-filter');
setPickerExpanded(true);
}
function updateSelectedItem(item) {
selectedItem = item || null;
if (!addSelectedIdInput || !addSubmit || !addSelection || !addSelectionName || !addSelectionMeta || !addSelectionImage) {
return;
}
if (!selectedItem) {
addSelectedIdInput.value = '';
addSubmit.disabled = true;
addSelection.classList.add('hidden-by-filter');
addSelectionName.textContent = '';
addSelectionMeta.textContent = '';
addSelectionImage.classList.add('hidden-by-filter');
addSelectionImage.setAttribute('src', '');
addSelectionImage.setAttribute('alt', '');
return;
}
addSelectedIdInput.value = String(selectedItem.id || '');
addSubmit.disabled = !addSelectedIdInput.value;
addSelection.classList.remove('hidden-by-filter');
addSelectionName.textContent = selectedItem.name || '';
var meta = selectedItem.type || '';
if (selectedItem.subtype) {
meta += (meta ? ' / ' : '') + selectedItem.subtype;
}
if (selectedItem.uuid) {
meta += (meta ? ' — ' : '') + selectedItem.uuid;
}
addSelectionMeta.textContent = meta;
if (selectedItem.uuid) {
addSelectionImage.classList.remove('hidden-by-filter');
addSelectionImage.setAttribute('src', 'https://cstone.space/uifimages/' + encodeURIComponent(selectedItem.uuid) + '.png');
addSelectionImage.setAttribute('alt', selectedItem.name || 'Objet sélectionné');
} else {
addSelectionImage.classList.add('hidden-by-filter');
addSelectionImage.setAttribute('src', '');
addSelectionImage.setAttribute('alt', '');
}
}
function renderPickerItems(items, query) {
if (!addDropdown) {
return;
}
if (!Array.isArray(items) || items.length === 0) {
addDropdown.innerHTML = '\n <div class="item-picker-option item-picker-option-empty" role="option" aria-disabled="true">\n <div class="search-item-content">\n <strong class="item-name">Aucun objet trouvé</strong>\n <div class="item-submeta">Aucun objet disponible pour “' + escapeHtml(query) + '”.</div>\n </div>\n </div>';
showPickerDropdown();
return;
}
addDropdown.innerHTML = items.map(function (item, index) {
var meta = escapeHtml(item.type || '');
if (item.subtype) {
meta += (meta ? ' / ' : '') + escapeHtml(item.subtype);
}
if (item.uuid) {
meta += (meta ? '<br>' : '') + escapeHtml(item.uuid);
}
return '' +
'<button type="button" class="item-picker-option' + (index === 0 ? ' is-active' : '') + '" role="option" data-item-id="' + escapeHtml(item.id) + '" data-item-name="' + escapeHtml(item.name) + '" data-item-type="' + escapeHtml(item.type || '') + '" data-item-subtype="' + escapeHtml(item.subtype || '') + '" data-item-uuid="' + escapeHtml(item.uuid || '') + '">' +
'<img src="https://cstone.space/uifimages/' + encodeURIComponent(item.uuid || '') + '.png" class="item-preview" alt="">' +
'<div class="search-item-content">' +
'<strong class="item-name">' + escapeHtml(item.name || '') + '</strong>' +
'<div class="item-submeta">' + meta + '</div>' +
'</div>' +
'</button>';
}).join('');
showPickerDropdown();
}
function fetchPickerSuggestions(query) {
if (!addDropdown) {
return;
}
var trimmedQuery = (query || '').trim();
if (trimmedQuery === '') {
hidePickerDropdown();
return;
}
var requestToken = ++pickerRequestToken;
addDropdown.innerHTML = '\n <div class="item-picker-option item-picker-option-empty" role="option" aria-disabled="true">\n <div class="search-item-content">\n <strong class="item-name">Recherche en cours...</strong>\n <div class="item-submeta">Chargement des objets disponibles.</div>\n </div>\n </div>';
showPickerDropdown();
fetch('scitemcustom.php?ajax=item_suggestions&q=' + encodeURIComponent(trimmedQuery), {
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
})
.then(function (response) {
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
return response.json();
})
.then(function (payload) {
if (requestToken !== pickerRequestToken) {
return;
}
renderPickerItems(payload && Array.isArray(payload.items) ? payload.items : [], trimmedQuery);
})
.catch(function () {
if (requestToken !== pickerRequestToken) {
return;
}
addDropdown.innerHTML = '\n <div class="item-picker-option item-picker-option-empty" role="option" aria-disabled="true">\n <div class="search-item-content">\n <strong class="item-name">Recherche indisponible</strong>\n <div class="item-submeta">Impossible de charger les suggestions pour le moment.</div>\n </div>\n </div>';
showPickerDropdown();
});
}
function clearSelection(options) {
var settings = options || {};
updateSelectedItem(null);
if (!settings.keepInput && addSearchInput) {
addSearchInput.value = '';
}
}
if (addForm && addSearchInput && addSelectedIdInput && addDropdown && addSelection && addSubmit && addReset && addClear) {
addSearchInput.addEventListener('input', function () {
clearSelection({ keepInput: true });
var query = addSearchInput.value || '';
window.clearTimeout(pickerRequestTimer);
pickerRequestTimer = window.setTimeout(function () {
fetchPickerSuggestions(query);
}, 120);
});
addSearchInput.addEventListener('focus', function () {
if ((addSearchInput.value || '').trim() !== '' && addDropdown.innerHTML.trim() !== '') {
showPickerDropdown();
}
});
addSearchInput.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
hidePickerDropdown();
}
});
addDropdown.addEventListener('click', function (event) {
var option = event.target.closest('.item-picker-option[data-item-id]');
if (!option) {
return;
}
var item = {
id: option.getAttribute('data-item-id') || '',
name: option.getAttribute('data-item-name') || '',
type: option.getAttribute('data-item-type') || '',
subtype: option.getAttribute('data-item-subtype') || '',
uuid: option.getAttribute('data-item-uuid') || ''
};
addSearchInput.value = item.name || '';
updateSelectedItem(item);
hidePickerDropdown();
});
document.addEventListener('click', function (event) {
if (!event.target.closest('#item-add-form')) {
hidePickerDropdown();
}
});
addReset.addEventListener('click', function () {
window.clearTimeout(pickerRequestTimer);
pickerRequestToken += 1;
clearSelection();
hidePickerDropdown();
addSearchInput.focus();
});
addClear.addEventListener('click', function () {
clearSelection();
hidePickerDropdown();
addSearchInput.focus();
});
addForm.addEventListener('submit', function (event) {
if (!addSelectedIdInput.value) {
event.preventDefault();
addSearchInput.focus();
}
});
}
function setCardState(card, isOpen) {
var body = card.querySelector('.custom-item-body');
var toggleButton = card.querySelector('.item-toggle-btn');
@ -1314,10 +1788,12 @@ $current_session_user = $_SESSION['user'] ?? '';
});
});
syncFromHash();
window.addEventListener('hashchange', syncFromHash);
if (itemCards.length > 0) {
syncFromHash();
window.addEventListener('hashchange', syncFromHash);
}
if (!filterInput || !resetButton || !visibleCount || !emptyState) {
if (!filterInput || !resetButton || !visibleCount || !emptyState || itemCards.length === 0) {
return;
}