Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 69 additions & 24 deletions app/Controllers/CollaneController.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,11 +430,47 @@ public function removeBook(Request $request, Response $response, mysqli $db): Re
}

/**
* API: search collane names for autocomplete.
* API: autocomplete suggestions for the book-form series fields.
*
* Query params:
* - `q` string the typed text (suggestions returned only for >= 2 chars)
* - `field` string which book-form field is asking; one of `collana`,
* `serie_padre`, `gruppo_serie`, `ciclo_serie`. Unknown values
* fall back to `collana`. The field selects the suggestion source
* (table + column) from an internal whitelist — never built from
* input — while only the search value and tipo are bound.
*
* @return Response JSON array of up to 10 distinct matching names.
*/
public function searchApi(Request $request, Response $response, mysqli $db): Response
{
$q = trim((string) ($request->getQueryParams()['q'] ?? ''));
$params = $request->getQueryParams();
$q = trim((string) ($params['q'] ?? ''));

// Each book-form series field suggests from its own existing values (the
// denormalized libri column) plus, when available, curated collane of the
// matching tipo — so e.g. the "universe" field proposes existing universes
// instead of forcing the user to retype them (#179). The column is
// whitelisted via $fieldMap (never taken from user input) so it can be
// embedded in the SQL safely; everything else is bound.
// Each book-form series field maps to one or more (table, column[, tipo])
// suggestion sources. Tables/columns are whitelisted here (never taken
// from user input) so they can be embedded in the SQL safely; the query
// value and any tipo filter are bound. This lets e.g. the "universe"
// field propose existing universes (collane.nome WHERE tipo='universo')
// instead of forcing the user to retype them (#179).
$sources = [
'collana' => [['t' => 'collane', 'c' => 'nome', 'tipos' => ['serie']],
['t' => 'libri', 'c' => 'collana', 'tipos' => []]],
'serie_padre' => [['t' => 'collane', 'c' => 'nome', 'tipos' => ['universo']]],
'gruppo_serie' => [['t' => 'collane', 'c' => 'gruppo_serie', 'tipos' => []]],
'ciclo_serie' => [['t' => 'collane', 'c' => 'ciclo', 'tipos' => []]],
];
$field = (string) ($params['field'] ?? 'collana');
if (!isset($sources[$field])) {
$field = 'collana';
}

$results = [];

// SEC2-1 + SEC1-5 (review): require min 2 chars and escape LIKE
Expand All @@ -443,31 +479,40 @@ public function searchApi(Request $request, Response $response, mysqli $db): Res
if (mb_strlen($q) >= 2) {
$escaped = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $q);
$search = '%' . $escaped . '%';
if ($this->hasCollaneTable($db)) {
$stmt = $db->prepare("
SELECT DISTINCT nome FROM (
SELECT nome FROM collane WHERE nome LIKE ?
UNION
SELECT collana AS nome FROM libri WHERE collana LIKE ? AND collana IS NOT NULL AND collana != '' AND deleted_at IS NULL
) AS combined ORDER BY nome LIMIT 10
");
if ($stmt) {
$stmt->bind_param('ss', $search, $search);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
$results[] = $row['nome'];

$hasCollane = $this->hasCollaneTable($db);
$sqlParts = [];
$types = '';
$binds = [];

foreach ($sources[$field] as $src) {
if ($src['t'] === 'collane' && !$hasCollane) {
continue; // no collane table — skip its sources
}
$col = $src['c'];
$where = "`{$col}` LIKE ? AND `{$col}` IS NOT NULL AND `{$col}` != ''";
$types .= 's';
$binds[] = $search;
if ($src['t'] === 'libri') {
$where .= ' AND deleted_at IS NULL';
}
if (!empty($src['tipos'])) {
$placeholders = implode(',', array_fill(0, count($src['tipos']), '?'));
$where .= " AND tipo IN ({$placeholders})";
$types .= str_repeat('s', count($src['tipos']));
foreach ($src['tipos'] as $t) {
$binds[] = $t;
}
$stmt->close();
}
} else {
$stmt = $db->prepare("
SELECT DISTINCT collana AS nome FROM libri
WHERE collana LIKE ? AND collana IS NOT NULL AND collana != '' AND deleted_at IS NULL
ORDER BY collana LIMIT 10
");
$sqlParts[] = "SELECT `{$col}` AS nome FROM {$src['t']} WHERE {$where}";
}

if (!empty($sqlParts)) {
$sql = 'SELECT DISTINCT nome FROM (' . implode(' UNION ', $sqlParts)
. ') AS combined ORDER BY nome LIMIT 10';
$stmt = $db->prepare($sql);
if ($stmt) {
$stmt->bind_param('s', $search);
$stmt->bind_param($types, ...$binds);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
Expand Down
138 changes: 127 additions & 11 deletions app/Views/libri/partials/book_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -504,13 +504,23 @@
<legend class="px-2 text-sm font-semibold text-gray-700"><i class="fas fa-layer-group text-primary mr-1"></i><?= __("Serie e collana") ?></legend>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label for="gruppo_serie" class="form-label"><?= __("Gruppo serie") ?></label>
<input id="gruppo_serie" name="gruppo_serie" type="text" class="form-input" placeholder="<?= htmlspecialchars(__('es. Fairy Tail'), ENT_QUOTES, 'UTF-8') ?>" value="<?php echo HtmlHelper::e($book['gruppo_serie'] ?? ''); ?>" aria-describedby="gruppo_serie_help" />
<label for="gruppo_serie_select" class="form-label"><?= __("Gruppo serie") ?></label>
<?php $gruppoSerieVal = (string)($book['gruppo_serie'] ?? ''); ?>
<select id="gruppo_serie_select" data-series-autocomplete="gruppo_serie" data-placeholder="<?= htmlspecialchars(__('es. Fairy Tail'), ENT_QUOTES, 'UTF-8') ?>" class="form-input" aria-describedby="gruppo_serie_help">
<option value=""></option>
<?php if ($gruppoSerieVal !== ''): ?><option value="<?= HtmlHelper::e($gruppoSerieVal) ?>" selected><?= HtmlHelper::e($gruppoSerieVal) ?></option><?php endif; ?>
</select>
<input type="hidden" id="gruppo_serie" name="gruppo_serie" value="<?php echo HtmlHelper::e($gruppoSerieVal); ?>" />
Comment on lines +511 to +513

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sostituisci HtmlHelper::e() nei nuovi campi autocomplete.

Questi valori finiscono in attributi HTML e testo visibile; le nuove righe devono usare direttamente htmlspecialchars(..., ENT_QUOTES, 'UTF-8').

Correzione proposta
-                  <?php if ($gruppoSerieVal !== ''): ?><option value="<?= HtmlHelper::e($gruppoSerieVal) ?>" selected><?= HtmlHelper::e($gruppoSerieVal) ?></option><?php endif; ?>
+                  <?php if ($gruppoSerieVal !== ''): ?><option value="<?= htmlspecialchars($gruppoSerieVal, ENT_QUOTES, 'UTF-8') ?>" selected><?= htmlspecialchars($gruppoSerieVal, ENT_QUOTES, 'UTF-8') ?></option><?php endif; ?>
                 </select>
-                <input type="hidden" id="gruppo_serie" name="gruppo_serie" value="<?php echo HtmlHelper::e($gruppoSerieVal); ?>" />
+                <input type="hidden" id="gruppo_serie" name="gruppo_serie" value="<?php echo htmlspecialchars($gruppoSerieVal, ENT_QUOTES, 'UTF-8'); ?>" />
...
-                  <?php if ($seriePadreVal !== ''): ?><option value="<?= HtmlHelper::e($seriePadreVal) ?>" selected><?= HtmlHelper::e($seriePadreVal) ?></option><?php endif; ?>
+                  <?php if ($seriePadreVal !== ''): ?><option value="<?= htmlspecialchars($seriePadreVal, ENT_QUOTES, 'UTF-8') ?>" selected><?= htmlspecialchars($seriePadreVal, ENT_QUOTES, 'UTF-8') ?></option><?php endif; ?>
                 </select>
-                <input type="hidden" id="serie_padre" name="serie_padre" value="<?php echo HtmlHelper::e($seriePadreVal); ?>" />
+                <input type="hidden" id="serie_padre" name="serie_padre" value="<?php echo htmlspecialchars($seriePadreVal, ENT_QUOTES, 'UTF-8'); ?>" />
...
-                  <?php if ($collanaVal !== ''): ?><option value="<?= HtmlHelper::e($collanaVal) ?>" selected><?= HtmlHelper::e($collanaVal) ?></option><?php endif; ?>
+                  <?php if ($collanaVal !== ''): ?><option value="<?= htmlspecialchars($collanaVal, ENT_QUOTES, 'UTF-8') ?>" selected><?= htmlspecialchars($collanaVal, ENT_QUOTES, 'UTF-8') ?></option><?php endif; ?>
                 </select>
-                <input type="hidden" id="collana" name="collana" value="<?php echo HtmlHelper::e($collanaVal); ?>" />
+                <input type="hidden" id="collana" name="collana" value="<?php echo htmlspecialchars($collanaVal, ENT_QUOTES, 'UTF-8'); ?>" />
...
-                  <?php if ($cicloSerieVal !== ''): ?><option value="<?= HtmlHelper::e($cicloSerieVal) ?>" selected><?= HtmlHelper::e($cicloSerieVal) ?></option><?php endif; ?>
+                  <?php if ($cicloSerieVal !== ''): ?><option value="<?= htmlspecialchars($cicloSerieVal, ENT_QUOTES, 'UTF-8') ?>" selected><?= htmlspecialchars($cicloSerieVal, ENT_QUOTES, 'UTF-8') ?></option><?php endif; ?>
                 </select>
-                <input type="hidden" id="ciclo_serie" name="ciclo_serie" value="<?php echo HtmlHelper::e($cicloSerieVal); ?>" />
+                <input type="hidden" id="ciclo_serie" name="ciclo_serie" value="<?php echo htmlspecialchars($cicloSerieVal, ENT_QUOTES, 'UTF-8'); ?>" />

As per path instructions, Mai usare HtmlHelper::e() nelle view — usare htmlspecialchars(..., ENT_QUOTES, 'UTF-8').

Also applies to: 521-523, 540-542, 557-559

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/libri/partials/book_form.php` around lines 511 - 513, Replace all
instances of HtmlHelper::e() with htmlspecialchars(..., ENT_QUOTES, 'UTF-8') in
the book_form.php view file. This applies to all occurrences of
HtmlHelper::e($gruppoSerieVal) in the option value attribute, option text
content, and hidden input value attribute across the specified line ranges
(511-513, 521-523, 540-542, 557-559). According to the codebase conventions,
HtmlHelper::e() should not be used in view files; instead use the native PHP
htmlspecialchars function with the appropriate ENT_QUOTES and UTF-8 parameters
for proper HTML escaping.

Source: Path instructions

<p id="gruppo_serie_help" class="text-xs text-gray-500 mt-1"><?= __('Etichetta "ombrello" per spin-off (es. tutto il franchise di Fairy Tail).') ?></p>
</div>
<div>
<label for="serie_padre" class="form-label"><?= __("Serie padre / universo") ?></label>
<input id="serie_padre" name="serie_padre" type="text" class="form-input" placeholder="<?= htmlspecialchars(__('es. I mondi di Aldebaran'), ENT_QUOTES, 'UTF-8') ?>" value="<?php echo HtmlHelper::e($book['serie_padre'] ?? ''); ?>" aria-describedby="serie_padre_help" />
<label for="serie_padre_select" class="form-label"><?= __("Serie padre / universo") ?></label>
<?php $seriePadreVal = (string)($book['serie_padre'] ?? ''); ?>
<select id="serie_padre_select" data-series-autocomplete="serie_padre" data-placeholder="<?= htmlspecialchars(__('es. I mondi di Aldebaran'), ENT_QUOTES, 'UTF-8') ?>" class="form-input" aria-describedby="serie_padre_help">
<option value=""></option>
<?php if ($seriePadreVal !== ''): ?><option value="<?= HtmlHelper::e($seriePadreVal) ?>" selected><?= HtmlHelper::e($seriePadreVal) ?></option><?php endif; ?>
</select>
<input type="hidden" id="serie_padre" name="serie_padre" value="<?php echo HtmlHelper::e($seriePadreVal); ?>" />
<p id="serie_padre_help" class="text-xs text-gray-500 mt-1"><?= __("Serie superiore nella gerarchia (es. l'universo che contiene cicli e stagioni).") ?></p>
</div>
<div>
Expand All @@ -523,8 +533,13 @@
<p id="tipo_collana_help" class="text-xs text-gray-500 mt-1"><?= __('Tassonomia: serie / universo / ciclo / stagione / spin-off / arco / collana editoriale.') ?></p>
</div>
<div>
<label for="collana" class="form-label"><?= __("Serie principale") ?></label>
<input id="collana" name="collana" type="text" class="form-input" placeholder="<?= htmlspecialchars(__('es. Fairy Tail: 100 Years Quest'), ENT_QUOTES, 'UTF-8') ?>" value="<?php echo HtmlHelper::e($book['collana'] ?? ''); ?>" aria-describedby="collana_help" />
<label for="collana_select" class="form-label"><?= __("Serie principale") ?></label>
<?php $collanaVal = (string)($book['collana'] ?? ''); ?>
<select id="collana_select" data-series-autocomplete="collana" data-placeholder="<?= htmlspecialchars(__('es. Fairy Tail: 100 Years Quest'), ENT_QUOTES, 'UTF-8') ?>" class="form-input" aria-describedby="collana_help">
<option value=""></option>
<?php if ($collanaVal !== ''): ?><option value="<?= HtmlHelper::e($collanaVal) ?>" selected><?= HtmlHelper::e($collanaVal) ?></option><?php endif; ?>
</select>
<input type="hidden" id="collana" name="collana" value="<?php echo HtmlHelper::e($collanaVal); ?>" />
<p id="collana_help" class="text-xs text-gray-500 mt-1"><?= __("Nome specifico della serie a cui appartiene il libro.") ?></p>
</div>
</div>
Expand All @@ -535,8 +550,13 @@
<input id="numero_serie" name="numero_serie" type="text" class="form-input" placeholder="<?= htmlspecialchars(__('es. 15'), ENT_QUOTES, 'UTF-8') ?>" value="<?php echo HtmlHelper::e($book['numero_serie'] ?? ''); ?>" />
</div>
<div>
<label for="ciclo_serie" class="form-label"><?= __("Ciclo / stagione") ?></label>
<input id="ciclo_serie" name="ciclo_serie" type="text" class="form-input" placeholder="<?= htmlspecialchars(__('es. Ciclo 1 - Aldebaran'), ENT_QUOTES, 'UTF-8') ?>" value="<?php echo HtmlHelper::e($book['ciclo_serie'] ?? ''); ?>" />
<label for="ciclo_serie_select" class="form-label"><?= __("Ciclo / stagione") ?></label>
<?php $cicloSerieVal = (string)($book['ciclo_serie'] ?? ''); ?>
<select id="ciclo_serie_select" data-series-autocomplete="ciclo_serie" data-placeholder="<?= htmlspecialchars(__('es. Ciclo 1 - Aldebaran'), ENT_QUOTES, 'UTF-8') ?>" class="form-input">
<option value=""></option>
<?php if ($cicloSerieVal !== ''): ?><option value="<?= HtmlHelper::e($cicloSerieVal) ?>" selected><?= HtmlHelper::e($cicloSerieVal) ?></option><?php endif; ?>
</select>
<input type="hidden" id="ciclo_serie" name="ciclo_serie" value="<?php echo HtmlHelper::e($cicloSerieVal); ?>" />
</div>
<div>
<label for="ordine_ciclo" class="form-label"><?= __("Ordine ciclo") ?></label>
Expand Down Expand Up @@ -1125,6 +1145,7 @@ function toggleLibraryThingAccordion() {
initializeUppy();
initializeChoicesJS();
initializePublishersChoices();
initializeSeriesAutocompletes();
initializeSweetAlert();
initializeGeneriDropdowns();
initializeFormValidation();
Expand Down Expand Up @@ -2129,6 +2150,96 @@ function initializeSweetAlert() {
*
* @returns {void}
*/
/**
* Single-value series autocompletes (#179): give the "universe / group / cycle /
* series" book-form fields the same Choices.js shape as the author/publisher
* pickers, so existing values are proposed after a couple of letters instead of
* being retyped (a typo no longer silently spawns a new universe). Each control
* is a <select data-series-autocomplete="<field>"> mirrored into a hidden input
* named <field> that carries the value to the form unchanged.
*/
function initializeSeriesAutocompletes() {
if (typeof Choices === 'undefined') return;
document.querySelectorAll('select[data-series-autocomplete]').forEach(function (sel) {
try {
const field = sel.getAttribute('data-series-autocomplete');
const hidden = document.getElementById(field);
if (!hidden) return;

const choice = new Choices(sel, {
searchEnabled: true,
shouldSort: false,
searchResultLimit: -1,
searchFloor: 2,
placeholder: true,
placeholderValue: sel.getAttribute('data-placeholder') || '',
itemSelectText: <?= json_encode(__("Clicca per selezionare"), JSON_HEX_TAG) ?>,
noChoicesText: <?= json_encode(__("Digita almeno 2 lettere per cercare o creare"), JSON_HEX_TAG) ?>,
classNames: { containerInner: 'choices__inner' }
});

const wrapper = sel.closest('.choices');
const input = wrapper ? wrapper.querySelector('.choices__input--cloned') : null;

const syncHidden = function () { hidden.value = choice.getValue(true) || ''; };
syncHidden();

// Commit a typed value (existing OR brand-new): a select-one Choices
// otherwise only lets the user pick pre-existing options.
const commitTyped = function (raw) {
const v = (raw || '').trim();
if (!v) return;
choice.setChoices([{ value: v, label: v, selected: true }], 'value', 'label', false);
hidden.value = v;
if (input) input.value = '';
choice.hideDropdown();
};

// Expose a setter so the ISBN scraper can fill this field visibly
// (it goes through Choices, not the now-hidden raw input).
window.__seriesAutocomplete = window.__seriesAutocomplete || {};
window.__seriesAutocomplete[field] = commitTyped;

let searchTimer = null;
sel.addEventListener('search', function (e) {
const q = (e.detail && e.detail.value ? e.detail.value : '').trim();
clearTimeout(searchTimer);
if (q.length < 2) return;
searchTimer = setTimeout(async function () {
try {
const resp = await fetch(`${window.BASE_PATH}/api/collane/search?field=${encodeURIComponent(field)}&q=${encodeURIComponent(q)}`, { credentials: 'same-origin' });
if (!resp.ok) return;
const names = await resp.json();
const opts = (Array.isArray(names) ? names : []).map(function (n) { return { value: n, label: n }; });
// Always offer the typed value so a NEW name can be created.
if (!opts.some(function (o) { return String(o.value).toLowerCase() === q.toLowerCase(); })) {
opts.unshift({ value: q, label: q });
}
choice.setChoices(opts, 'value', 'label', true);
} catch (err) { console.error('series autocomplete failed:', err); }
}, 300);
});

sel.addEventListener('change', syncHidden);
// Don't lose a typed-but-not-picked value: commit it on blur.
if (input) {
input.addEventListener('blur', function () { if (input.value.trim()) commitTyped(input.value); });
}
// Enter on typed text with nothing highlighted creates/commits it.
if (typeof choice._onEnterKey === 'function') {
const origEnter = choice._onEnterKey.bind(choice);
choice._onEnterKey = function (event, hasActiveDropdown) {
const typed = input ? input.value.trim() : '';
const dd = wrapper ? wrapper.querySelector('.choices__list--dropdown') : null;
const hl = dd ? dd.querySelector('.choices__item--selectable.is-highlighted') : null;
if (typed && !hl) { event.preventDefault(); commitTyped(typed); return; }
return origEnter(event, hasActiveDropdown);
};
Comment on lines +2229 to +2237

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify file exists and check its structure
if [ -f "app/Views/libri/partials/book_form.php" ]; then
  wc -l "app/Views/libri/partials/book_form.php"
  echo "---"
  # Get lines around 2229-2237 to see the actual code
  sed -n '2200,2250p' "app/Views/libri/partials/book_form.php"
else
  echo "File not found"
  find . -name "book_form.php" -type f
fi

Repository: fabiodalez-dev/Pinakes

Length of output: 3063


🏁 Script executed:

# Search for Choices library usage and DOM structure references
rg "choices__item" "app/Views" -A 2 -B 2

echo "---"

# Search for any other references to .choices__item-text
rg "choices__item-text" . --type html --type php --type js

echo "---"

# Check if Choices is imported and what version/configuration
rg "Choices\|choices\.js" "app/Views/libri/partials/book_form.php" -B 5 -A 5 | head -30

Repository: fabiodalez-dev/Pinakes

Length of output: 8039


🏁 Script executed:

# Get exact context around line 2229 to confirm if it's still the unfixed version
sed -n '2215,2245p' "app/Views/libri/partials/book_form.php" | cat -n

Repository: fabiodalez-dev/Pinakes

Length of output: 2030


Il campo serie committerà il suggerimento sbagliato se l'utente digita un nome diverso da quello evidenziato e preme Enter.

Quando la lista dropdown ha un elemento evidenziato (.is-highlighted), il codice delega a origEnter() che può selezionare il suggerimento sbagliato, anche se l'utente ha digitato un valore diverso. Questo causa il salvataggio di dati errati nel campo hidden.

La correzione è già implementata correttamente in initializePublishersChoices() (estrae il testo evidenziato, lo confronta con quello digitato, previene il default se diverso). Applica lo stesso pattern al metodo _onEnterKey della serie autocomplete.

Correzione proposta
                choice._onEnterKey = function (event, hasActiveDropdown) {
                    const typed = input ? input.value.trim() : '';
                    const dd = wrapper ? wrapper.querySelector('.choices__list--dropdown') : null;
                    const hl = dd ? dd.querySelector('.choices__item--selectable.is-highlighted') : null;
-                    if (typed && !hl) { event.preventDefault(); commitTyped(typed); return; }
+                    if (typed && hl) {
+                        const nameEl = hl.querySelector('.choices__item-text') || hl.childNodes[0];
+                        const highlightedText = (nameEl ? nameEl.textContent : hl.textContent).trim().toLowerCase();
+                        if (highlightedText !== typed.toLowerCase()) {
+                            event.preventDefault();
+                            commitTyped(typed);
+                            return;
+                        }
+                    }
+                    if (typed && !hl) { event.preventDefault(); commitTyped(typed); return; }
                    return origEnter(event, hasActiveDropdown);
                };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof choice._onEnterKey === 'function') {
const origEnter = choice._onEnterKey.bind(choice);
choice._onEnterKey = function (event, hasActiveDropdown) {
const typed = input ? input.value.trim() : '';
const dd = wrapper ? wrapper.querySelector('.choices__list--dropdown') : null;
const hl = dd ? dd.querySelector('.choices__item--selectable.is-highlighted') : null;
if (typed && !hl) { event.preventDefault(); commitTyped(typed); return; }
return origEnter(event, hasActiveDropdown);
};
if (typeof choice._onEnterKey === 'function') {
const origEnter = choice._onEnterKey.bind(choice);
choice._onEnterKey = function (event, hasActiveDropdown) {
const typed = input ? input.value.trim() : '';
const dd = wrapper ? wrapper.querySelector('.choices__list--dropdown') : null;
const hl = dd ? dd.querySelector('.choices__item--selectable.is-highlighted') : null;
if (typed && hl) {
const nameEl = hl.querySelector('.choices__item-text') || hl.childNodes[0];
const highlightedText = (nameEl ? nameEl.textContent : hl.textContent).trim().toLowerCase();
if (highlightedText !== typed.toLowerCase()) {
event.preventDefault();
commitTyped(typed);
return;
}
}
if (typed && !hl) { event.preventDefault(); commitTyped(typed); return; }
return origEnter(event, hasActiveDropdown);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/libri/partials/book_form.php` around lines 2229 - 2237, The
_onEnterKey override for the series field does not handle the case where the
user types a value different from the highlighted dropdown item. The current
logic only commits the typed value when there is no highlighted item, but
delegates to origEnter when a highlighted item exists, which incorrectly selects
the wrong suggestion. Extract the text content of the highlighted item (hl),
compare it with the typed value, and if they differ, call event.preventDefault()
and commitTyped(typed) to prevent the wrong suggestion from being selected. This
should mirror the same pattern already correctly implemented in
initializePublishersChoices() for publisher selection, ensuring the user's typed
input is preserved instead of the highlighted dropdown suggestion when they
differ.

}
} catch (err) { console.error('initializeSeriesAutocompletes:', err); }
});
}

function initializePublishersChoices() {
try {
const element = document.getElementById('editori_select');
Expand Down Expand Up @@ -3869,9 +3980,14 @@ function initializeIsbnImport() {
// Handle series (collana)
try {
if (data.series) {
const seriesInput = document.querySelector('input[name="collana"]');
if (seriesInput) {
seriesInput.value = data.series;
// collana is now a Choices autocomplete (#179) — set it via the
// exposed setter so the value shows in the control; fall back to
// the hidden input if the autocomplete hasn't initialised.
if (window.__seriesAutocomplete && window.__seriesAutocomplete.collana) {
window.__seriesAutocomplete.collana(data.series);
} else {
const seriesInput = document.getElementById('collana');
if (seriesInput) seriesInput.value = data.series;
}
const scrapedSeries = document.getElementById('scraped_series');
if (scrapedSeries) {
Expand Down
Loading
Loading