From a1559c3861cf1208c5a0cb1a9d001f37ebdd0be6 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 28 Apr 2026 22:51:04 +0000 Subject: [PATCH] Autosave: 20260428-225104 --- scitemcustom.php | 538 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 507 insertions(+), 31 deletions(-) diff --git a/scitemcustom.php b/scitemcustom.php index 8375929..fb6f529 100644 --- a/scitemcustom.php +++ b/scitemcustom.php @@ -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'] ?? '';

Ajouter un objet

Choisis un objet depuis la base, puis attribue-lui autant de stats que nécessaire.

-
- - - Reset + + + + + + +
+ +
+
+ +
Commence à taper le nom d'un objet. La liste juste en dessous se met à jour dynamiquement et tu peux cliquer sur l'objet voulu.
+ +
+
+ +
+ +
+
+
+ +
+ +
+ + +
+ + -
Résultats disponibles pour l'ajout dans la liste Item Custom.
+
Résultats de la recherche classique pour l'ajout dans la liste Item Custom.
@@ -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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + 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
\n
\n Aucun objet trouvé\n
Aucun objet disponible pour “' + escapeHtml(query) + '”.
\n
\n
'; + 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 ? '
' : '') + escapeHtml(item.uuid); + } + + return '' + + ''; + }).join(''); + + showPickerDropdown(); + } + + function fetchPickerSuggestions(query) { + if (!addDropdown) { + return; + } + + var trimmedQuery = (query || '').trim(); + if (trimmedQuery === '') { + hidePickerDropdown(); + return; + } + + var requestToken = ++pickerRequestToken; + addDropdown.innerHTML = '\n
\n
\n Recherche en cours...\n
Chargement des objets disponibles.
\n
\n
'; + 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
\n
\n Recherche indisponible\n
Impossible de charger les suggestions pour le moment.
\n
\n
'; + 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; }