Add searchable manufacturer picker
This commit is contained in:
@@ -2,6 +2,11 @@
|
|||||||
const fieldGrid = document.querySelector("#fieldGrid");
|
const fieldGrid = document.querySelector("#fieldGrid");
|
||||||
const variantHeader = document.querySelector("#variantHeader");
|
const variantHeader = document.querySelector("#variantHeader");
|
||||||
const manufacturerSelect = document.querySelector("#manufacturerSelect");
|
const manufacturerSelect = document.querySelector("#manufacturerSelect");
|
||||||
|
const manufacturerTrigger = document.querySelector("#manufacturerTrigger");
|
||||||
|
const manufacturerMenu = document.querySelector("#manufacturerMenu");
|
||||||
|
const manufacturerSearch = document.querySelector("#manufacturerSearch");
|
||||||
|
const manufacturerOptions = document.querySelector("#manufacturerOptions");
|
||||||
|
const manufacturerValue = document.querySelector("#manufacturerValue");
|
||||||
const languageSelect = document.querySelector("#languageSelect");
|
const languageSelect = document.querySelector("#languageSelect");
|
||||||
const recordIdElement = document.querySelector("#recordId");
|
const recordIdElement = document.querySelector("#recordId");
|
||||||
const loadProductButton = document.querySelector("#loadProductButton");
|
const loadProductButton = document.querySelector("#loadProductButton");
|
||||||
@@ -129,6 +134,8 @@ setTableWidth();
|
|||||||
await loadManufacturers();
|
await loadManufacturers();
|
||||||
await loadLanguages();
|
await loadLanguages();
|
||||||
manufacturerSelect.addEventListener("change", () => {
|
manufacturerSelect.addEventListener("change", () => {
|
||||||
|
syncManufacturerPickerLabel();
|
||||||
|
closeManufacturerPicker();
|
||||||
if (manufacturerSelect.value) {
|
if (manufacturerSelect.value) {
|
||||||
loadMappingSources();
|
loadMappingSources();
|
||||||
loadProductForSelectedManufacturer(1);
|
loadProductForSelectedManufacturer(1);
|
||||||
@@ -138,6 +145,16 @@ manufacturerSelect.addEventListener("change", () => {
|
|||||||
clearProductState("Select manufacturer to load data.");
|
clearProductState("Select manufacturer to load data.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
manufacturerTrigger.addEventListener("click", toggleManufacturerPicker);
|
||||||
|
manufacturerSearch.addEventListener("input", renderManufacturerOptions);
|
||||||
|
manufacturerSearch.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape")
|
||||||
|
closeManufacturerPicker();
|
||||||
|
});
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (!event.target.closest("#manufacturerPicker"))
|
||||||
|
closeManufacturerPicker();
|
||||||
|
});
|
||||||
loadProductButton.disabled = true;
|
loadProductButton.disabled = true;
|
||||||
loadProductButton.title = "Load existing product attribute info.";
|
loadProductButton.title = "Load existing product attribute info.";
|
||||||
loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
|
loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
|
||||||
@@ -258,9 +275,59 @@ async function loadManufacturers() {
|
|||||||
}
|
}
|
||||||
if (data.message)
|
if (data.message)
|
||||||
manufacturerSelect.append(createOption("", data.message));
|
manufacturerSelect.append(createOption("", data.message));
|
||||||
|
syncManufacturerPickerLabel();
|
||||||
|
renderManufacturerOptions();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
manufacturerSelect.append(createOption("", error.message || "Local DB is not configured."));
|
manufacturerSelect.append(createOption("", error.message || "Local DB is not configured."));
|
||||||
|
syncManufacturerPickerLabel();
|
||||||
|
renderManufacturerOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toggleManufacturerPicker() {
|
||||||
|
if (manufacturerMenu.hidden) {
|
||||||
|
manufacturerMenu.hidden = false;
|
||||||
|
manufacturerTrigger.setAttribute("aria-expanded", "true");
|
||||||
|
manufacturerSearch.value = "";
|
||||||
|
renderManufacturerOptions();
|
||||||
|
if (manufacturerSelect.options.length > 11)
|
||||||
|
manufacturerSearch.focus();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
closeManufacturerPicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function closeManufacturerPicker() {
|
||||||
|
manufacturerMenu.hidden = true;
|
||||||
|
manufacturerTrigger.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
function syncManufacturerPickerLabel() {
|
||||||
|
const option = manufacturerSelect.selectedOptions[0];
|
||||||
|
manufacturerValue.textContent = option?.textContent || "Select manufacturer";
|
||||||
|
}
|
||||||
|
function renderManufacturerOptions() {
|
||||||
|
const options = [...manufacturerSelect.options].filter((option) => option.value);
|
||||||
|
const query = manufacturerSearch.value.trim().toLocaleLowerCase();
|
||||||
|
const filtered = options.filter((option) => option.textContent.toLocaleLowerCase().includes(query));
|
||||||
|
const visible = filtered.slice(0, 10);
|
||||||
|
manufacturerOptions.replaceChildren(...visible.map((option) => {
|
||||||
|
const item = document.createElement("button");
|
||||||
|
item.className = "manufacturer-option";
|
||||||
|
item.type = "button";
|
||||||
|
item.setAttribute("role", "option");
|
||||||
|
item.textContent = option.textContent;
|
||||||
|
item.setAttribute("aria-selected", String(option.value === manufacturerSelect.value));
|
||||||
|
item.addEventListener("click", () => {
|
||||||
|
manufacturerSelect.value = option.value;
|
||||||
|
manufacturerSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
});
|
||||||
|
return item;
|
||||||
|
}));
|
||||||
|
if (!visible.length) {
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
empty.className = "manufacturer-empty";
|
||||||
|
empty.textContent = "No manufacturer found.";
|
||||||
|
manufacturerOptions.append(empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function createOption(value, label) {
|
function createOption(value, label) {
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
const fieldGrid = document.querySelector("#fieldGrid");
|
const fieldGrid = document.querySelector("#fieldGrid");
|
||||||
const variantHeader = document.querySelector("#variantHeader");
|
const variantHeader = document.querySelector("#variantHeader");
|
||||||
const manufacturerSelect = document.querySelector("#manufacturerSelect");
|
const manufacturerSelect = document.querySelector("#manufacturerSelect");
|
||||||
|
const manufacturerTrigger = document.querySelector("#manufacturerTrigger");
|
||||||
|
const manufacturerMenu = document.querySelector("#manufacturerMenu");
|
||||||
|
const manufacturerSearch = document.querySelector("#manufacturerSearch");
|
||||||
|
const manufacturerOptions = document.querySelector("#manufacturerOptions");
|
||||||
|
const manufacturerValue = document.querySelector("#manufacturerValue");
|
||||||
const languageSelect = document.querySelector("#languageSelect");
|
const languageSelect = document.querySelector("#languageSelect");
|
||||||
const recordIdElement = document.querySelector("#recordId");
|
const recordIdElement = document.querySelector("#recordId");
|
||||||
const loadProductButton = document.querySelector("#loadProductButton");
|
const loadProductButton = document.querySelector("#loadProductButton");
|
||||||
@@ -136,6 +141,8 @@ await loadManufacturers();
|
|||||||
await loadLanguages();
|
await loadLanguages();
|
||||||
|
|
||||||
manufacturerSelect.addEventListener("change", () => {
|
manufacturerSelect.addEventListener("change", () => {
|
||||||
|
syncManufacturerPickerLabel();
|
||||||
|
closeManufacturerPicker();
|
||||||
if (manufacturerSelect.value) {
|
if (manufacturerSelect.value) {
|
||||||
loadMappingSources();
|
loadMappingSources();
|
||||||
loadProductForSelectedManufacturer(1);
|
loadProductForSelectedManufacturer(1);
|
||||||
@@ -145,6 +152,15 @@ manufacturerSelect.addEventListener("change", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
manufacturerTrigger.addEventListener("click", toggleManufacturerPicker);
|
||||||
|
manufacturerSearch.addEventListener("input", renderManufacturerOptions);
|
||||||
|
manufacturerSearch.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") closeManufacturerPicker();
|
||||||
|
});
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (!event.target.closest("#manufacturerPicker")) closeManufacturerPicker();
|
||||||
|
});
|
||||||
|
|
||||||
loadProductButton.disabled = true;
|
loadProductButton.disabled = true;
|
||||||
loadProductButton.title = "Load existing product attribute info.";
|
loadProductButton.title = "Load existing product attribute info.";
|
||||||
loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
|
loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
|
||||||
@@ -272,8 +288,64 @@ async function loadManufacturers() {
|
|||||||
manufacturerSelect.append(createOption(String(manufacturer.id), manufacturer.name));
|
manufacturerSelect.append(createOption(String(manufacturer.id), manufacturer.name));
|
||||||
}
|
}
|
||||||
if (data.message) manufacturerSelect.append(createOption("", data.message));
|
if (data.message) manufacturerSelect.append(createOption("", data.message));
|
||||||
|
syncManufacturerPickerLabel();
|
||||||
|
renderManufacturerOptions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
manufacturerSelect.append(createOption("", error.message || "Local DB is not configured."));
|
manufacturerSelect.append(createOption("", error.message || "Local DB is not configured."));
|
||||||
|
syncManufacturerPickerLabel();
|
||||||
|
renderManufacturerOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleManufacturerPicker() {
|
||||||
|
if (manufacturerMenu.hidden) {
|
||||||
|
manufacturerMenu.hidden = false;
|
||||||
|
manufacturerTrigger.setAttribute("aria-expanded", "true");
|
||||||
|
manufacturerSearch.value = "";
|
||||||
|
renderManufacturerOptions();
|
||||||
|
if (manufacturerSelect.options.length > 11) manufacturerSearch.focus();
|
||||||
|
} else {
|
||||||
|
closeManufacturerPicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeManufacturerPicker() {
|
||||||
|
manufacturerMenu.hidden = true;
|
||||||
|
manufacturerTrigger.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncManufacturerPickerLabel() {
|
||||||
|
const option = manufacturerSelect.selectedOptions[0];
|
||||||
|
manufacturerValue.textContent = option?.textContent || "Select manufacturer";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderManufacturerOptions() {
|
||||||
|
const options = [...manufacturerSelect.options].filter((option) => option.value);
|
||||||
|
const query = manufacturerSearch.value.trim().toLocaleLowerCase();
|
||||||
|
const filtered = options.filter((option) => option.textContent.toLocaleLowerCase().includes(query));
|
||||||
|
const visible = filtered.slice(0, 10);
|
||||||
|
|
||||||
|
manufacturerOptions.replaceChildren(
|
||||||
|
...visible.map((option) => {
|
||||||
|
const item = document.createElement("button");
|
||||||
|
item.className = "manufacturer-option";
|
||||||
|
item.type = "button";
|
||||||
|
item.setAttribute("role", "option");
|
||||||
|
item.textContent = option.textContent;
|
||||||
|
item.setAttribute("aria-selected", String(option.value === manufacturerSelect.value));
|
||||||
|
item.addEventListener("click", () => {
|
||||||
|
manufacturerSelect.value = option.value;
|
||||||
|
manufacturerSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
});
|
||||||
|
return item;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!visible.length) {
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
empty.className = "manufacturer-empty";
|
||||||
|
empty.textContent = "No manufacturer found.";
|
||||||
|
manufacturerOptions.append(empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -60,7 +60,17 @@
|
|||||||
<option value="">Select language</option>
|
<option value="">Select language</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select class="input-like" id="manufacturerSelect" aria-label="Manufacturer">
|
<div class="manufacturer-picker" id="manufacturerPicker">
|
||||||
|
<button class="manufacturer-trigger" id="manufacturerTrigger" type="button" aria-haspopup="listbox" aria-expanded="false">
|
||||||
|
<span id="manufacturerValue">Select manufacturer</span>
|
||||||
|
<span class="picker-chevron" aria-hidden="true">⌄</span>
|
||||||
|
</button>
|
||||||
|
<div class="manufacturer-menu" id="manufacturerMenu" hidden>
|
||||||
|
<input class="manufacturer-search" id="manufacturerSearch" type="search" placeholder="Search manufacturer..." aria-label="Search manufacturer" />
|
||||||
|
<div class="manufacturer-options" id="manufacturerOptions" role="listbox"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select class="input-like native-manufacturer-select" id="manufacturerSelect" aria-label="Manufacturer">
|
||||||
<option value="">Select manufacturer</option>
|
<option value="">Select manufacturer</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -76,6 +76,20 @@
|
|||||||
.arrow-button:hover:not(:disabled), .action-button:hover:not(:disabled) { @apply border-slate-500 bg-slate-700 text-white; }
|
.arrow-button:hover:not(:disabled), .action-button:hover:not(:disabled) { @apply border-slate-500 bg-slate-700 text-white; }
|
||||||
.arrow-button:disabled, .action-button:disabled { @apply cursor-not-allowed opacity-50; }
|
.arrow-button:disabled, .action-button:disabled { @apply cursor-not-allowed opacity-50; }
|
||||||
.input-like { @apply h-7 px-1 text-[11px]; }
|
.input-like { @apply h-7 px-1 text-[11px]; }
|
||||||
|
.native-manufacturer-select { @apply hidden; }
|
||||||
|
.manufacturer-picker { @apply relative; }
|
||||||
|
.manufacturer-trigger { @apply flex h-7 w-full items-center justify-between rounded border border-slate-700 bg-slate-800 px-2 text-[11px] text-slate-200 shadow-sm; }
|
||||||
|
.manufacturer-trigger:hover { @apply border-slate-500 bg-slate-700; }
|
||||||
|
.manufacturer-trigger[aria-expanded="true"] { @apply border-catalog-focus ring-2 ring-catalog-focus/25; }
|
||||||
|
.manufacturer-trigger span:first-child { @apply min-w-0 truncate; }
|
||||||
|
.picker-chevron { @apply ml-2 text-slate-400; }
|
||||||
|
.manufacturer-menu { @apply absolute left-0 right-0 top-full z-50 mt-1 rounded border border-slate-300 bg-white p-1 shadow-xl; }
|
||||||
|
.manufacturer-menu[hidden] { display: none; }
|
||||||
|
.manufacturer-search { @apply mb-1 h-7 w-full rounded border border-slate-300 px-2 text-[11px] text-slate-800 outline-none focus:border-catalog-focus focus:ring-2 focus:ring-catalog-focus/25; }
|
||||||
|
.manufacturer-options { @apply max-h-60 overflow-y-auto; }
|
||||||
|
.manufacturer-option { @apply block min-h-7 w-full truncate rounded px-2 py-1 text-left text-[11px] text-slate-700 hover:bg-slate-100; }
|
||||||
|
.manufacturer-option[aria-selected="true"] { @apply bg-slate-200 font-medium text-slate-900; }
|
||||||
|
.manufacturer-empty { @apply px-2 py-2 text-[11px] text-slate-500; }
|
||||||
#scanInput { @apply bg-slate-700 placeholder:text-slate-400; }
|
#scanInput { @apply bg-slate-700 placeholder:text-slate-400; }
|
||||||
.input-like:focus { @apply border-catalog-focus outline-none ring-2 ring-catalog-focus/25; }
|
.input-like:focus { @apply border-catalog-focus outline-none ring-2 ring-catalog-focus/25; }
|
||||||
.picture-slot { @apply aspect-square w-full overflow-hidden rounded border border-dashed border-slate-500 bg-catalog-panel-soft p-1 text-center text-[11px] text-slate-200; }
|
.picture-slot { @apply aspect-square w-full overflow-hidden rounded border border-dashed border-slate-500 bg-catalog-panel-soft p-1 text-center text-[11px] text-slate-200; }
|
||||||
@@ -205,6 +219,8 @@
|
|||||||
html:not(.dark) .mapping-box { @apply border-slate-300 bg-white text-slate-600; }
|
html:not(.dark) .mapping-box { @apply border-slate-300 bg-white text-slate-600; }
|
||||||
html:not(.dark) .mapping-toggle::before { @apply text-slate-700; }
|
html:not(.dark) .mapping-toggle::before { @apply text-slate-700; }
|
||||||
html:not(.dark) .app-status { @apply border-slate-300 bg-white text-slate-700; }
|
html:not(.dark) .app-status { @apply border-slate-300 bg-white text-slate-700; }
|
||||||
|
html:not(.dark) .manufacturer-trigger { @apply border-slate-300 bg-white text-slate-700; }
|
||||||
|
html:not(.dark) .manufacturer-trigger:hover { @apply border-slate-400 bg-slate-200; }
|
||||||
|
|
||||||
html.dark body { @apply bg-slate-950 text-slate-100; }
|
html.dark body { @apply bg-slate-950 text-slate-100; }
|
||||||
html.dark .control-panel { @apply bg-slate-950; }
|
html.dark .control-panel { @apply bg-slate-950; }
|
||||||
@@ -223,6 +239,10 @@
|
|||||||
html.dark .arrow-button:hover:not(:disabled),
|
html.dark .arrow-button:hover:not(:disabled),
|
||||||
html.dark .action-button:hover:not(:disabled) { @apply border-slate-400 bg-slate-700; }
|
html.dark .action-button:hover:not(:disabled) { @apply border-slate-400 bg-slate-700; }
|
||||||
html.dark #scanInput { @apply bg-slate-700 placeholder:text-slate-400; }
|
html.dark #scanInput { @apply bg-slate-700 placeholder:text-slate-400; }
|
||||||
|
html.dark .manufacturer-menu { @apply border-slate-700 bg-slate-900; }
|
||||||
|
html.dark .manufacturer-search { @apply border-slate-600 bg-slate-800 text-slate-100; }
|
||||||
|
html.dark .manufacturer-option { @apply text-slate-200 hover:bg-slate-800; }
|
||||||
|
html.dark .manufacturer-option[aria-selected="true"] { @apply bg-slate-700 text-white; }
|
||||||
html.dark .settings-dialog,
|
html.dark .settings-dialog,
|
||||||
html.dark .suggested-dialog,
|
html.dark .suggested-dialog,
|
||||||
html.dark .source-dialog { @apply border-slate-700 bg-slate-900; }
|
html.dark .source-dialog { @apply border-slate-700 bg-slate-900; }
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ test("settings modal opens and shows local database controls", async ({ page })
|
|||||||
await expect(page.locator("#testLocalDbButton")).toBeVisible();
|
await expect(page.locator("#testLocalDbButton")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("manufacturer picker shows ten results and supports search", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
await page.locator("#manufacturerTrigger").click();
|
||||||
|
|
||||||
|
await expect(page.locator("#manufacturerOptions .manufacturer-option")).toHaveCount(10);
|
||||||
|
await expect(page.locator("#manufacturerSearch")).toBeVisible();
|
||||||
|
await page.locator("#manufacturerSearch").fill("La Sportiva");
|
||||||
|
await expect(page.locator("#manufacturerOptions .manufacturer-option")).toHaveText(["La Sportiva"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("catalog maker remains usable on a narrow viewport", async ({ page }) => {
|
test("catalog maker remains usable on a narrow viewport", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|||||||
Reference in New Issue
Block a user