[object Object]

← back to Dw Hero Admin

fix(filter): preserve focus on prodFilter — split renderProducts() into renderProductsHeader() + renderProductsGrid()

4adc64635fa86a824bcbdac703a1c52646826b46 · 2026-05-14 08:41:55 -0700 · SteveStudio2

Old code wiped productsWrap.innerHTML on every keystroke, which destroyed the input the user was typing into and stole focus after each character. Header is now rendered once on site-pick; grid + count update on each filter keystroke, but the input DOM node and its caret are untouched.

Files touched

Diff

commit 4adc64635fa86a824bcbdac703a1c52646826b46
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu May 14 08:41:55 2026 -0700

    fix(filter): preserve focus on prodFilter — split renderProducts() into renderProductsHeader() + renderProductsGrid()
    
    Old code wiped productsWrap.innerHTML on every keystroke, which destroyed the input the user was typing into and stole focus after each character. Header is now rendered once on site-pick; grid + count update on each filter keystroke, but the input DOM node and its caret are untouched.
---
 public/index.html | 38 +++++++++++++++++++++++++-------------
 1 file changed, 25 insertions(+), 13 deletions(-)

diff --git a/public/index.html b/public/index.html
index 4185dd0..0c32b51 100644
--- a/public/index.html
+++ b/public/index.html
@@ -166,7 +166,8 @@
       els.siteMeta.textContent = data.productCount + ' products · newest first';
       els.slotsWrap.style.display = 'block';
       renderSlots();
-      renderProducts();
+      renderProductsHeader();
+      renderProductsGrid();
     }).catch(function (e) {
       els.productsWrap.innerHTML = '<div class="empty-state">Error: ' + e.message + '</div>';
     });
@@ -244,20 +245,35 @@
     flashStatus('unsaved', '');
   }
 
-  function renderProducts() {
-    var q = (state.filter || '').toLowerCase();
-    var items = state.products.filter(function (p) {
-      if (!q) return true;
-      return ((p.title || '') + ' ' + (p.sku || '') + ' ' + (p.vendor || '') + ' ' + (p.tags || []).join(' ')).toLowerCase().indexOf(q) !== -1;
-    });
+  // Header is rendered ONCE per site pick — preserves focus on the search
+  // input across keystroke-driven re-renders of the grid.
+  function renderProductsHeader() {
     els.productsWrap.innerHTML = '\
       <div class="products-header">\
-        <h3>All products — newest first (' + items.length + ')</h3>\
-        <div class="product-search"><input id="prodFilter" type="search" placeholder="Filter pattern, vendor, tag..." value="' + escapeHtml(q) + '"></div>\
+        <h3 id="prodCount">All products — newest first</h3>\
+        <div class="product-search"><input id="prodFilter" type="search" placeholder="Filter pattern, vendor, tag..." value="' + escapeHtml(state.filter || '') + '"></div>\
       </div>\
       <div class="products" id="prodGrid"></div>\
     ';
+    var pf = document.getElementById('prodFilter');
+    pf.addEventListener('input', function () {
+      state.filter = pf.value;
+      renderProductsGrid();
+    });
+  }
+
+  // Only re-renders the grid + count — never touches the header DOM, so
+  // the user's keystroke focus stays in #prodFilter while typing.
+  function renderProductsGrid() {
+    var q = (state.filter || '').toLowerCase();
+    var items = state.products.filter(function (p) {
+      if (!q) return true;
+      return ((p.title || '') + ' ' + (p.sku || '') + ' ' + (p.vendor || '') + ' ' + (p.tags || []).join(' ')).toLowerCase().indexOf(q) !== -1;
+    });
+    var count = document.getElementById('prodCount');
+    if (count) count.textContent = 'All products — newest first (' + items.length + ')';
     var grid = document.getElementById('prodGrid');
+    if (!grid) return;
     grid.innerHTML = items.slice(0, 400).map(function (p) {
       return '<div class="product" draggable="true" data-sku="' + escapeHtml(p.sku) + '">\
         <img src="' + escapeHtml(p.image_url) + '" alt="' + escapeHtml(p.title || '') + '" loading="lazy">\
@@ -272,10 +288,6 @@
       });
       el.addEventListener('dragend', function () { el.classList.remove('dragging'); });
     });
-    var pf = document.getElementById('prodFilter');
-    if (pf) {
-      pf.addEventListener('input', function () { state.filter = pf.value; renderProducts(); });
-    }
   }
 
   function flashStatus(msg, cls) {

← ea04db8 initial scaffold — DW hero-grid admin :9772 with drag-drop i  ·  back to Dw Hero Admin  ·  css: flex-shrink:0 on .topbar and .slots-wrap 393ad7b →