Skip to content

Commit 3d59b2b

Browse files
itsDNNSclaude
andcommitted
Replace native ISP select with custom dropdown showing brand badges
Native <select> elements don't support HTML in options. Replace with a custom div-based dropdown that shows colored rounded rectangles (brand badges) before each ISP name in the option list and selected display. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d07e92d commit 3d59b2b

File tree

2 files changed

+177
-41
lines changed

2 files changed

+177
-41
lines changed

app/templates/settings.html

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,31 @@
131131
color: var(--crit); padding: 10px; border-radius: 6px;
132132
margin-bottom: 16px; display: none;
133133
}
134+
.isp-dropdown { position: relative; }
135+
.isp-selected {
136+
display: flex; align-items: center; gap: 8px;
137+
background: var(--input-bg); border: 1px solid var(--input-border);
138+
border-radius: 4px; padding: 8px 10px; cursor: pointer;
139+
font-size: 0.95em; font-family: inherit; color: var(--text);
140+
}
141+
.isp-selected:hover { border-color: var(--accent); }
142+
.isp-selected .isp-arrow { margin-left: auto; font-size: 0.8em; color: var(--muted); }
143+
.isp-options {
144+
display: none; position: absolute; top: 100%; left: 0; right: 0;
145+
background: var(--input-bg); border: 1px solid var(--input-border);
146+
border-radius: 4px; margin-top: 2px; z-index: 50;
147+
max-height: 240px; overflow-y: auto;
148+
}
149+
.isp-options.open { display: block; }
150+
.isp-option {
151+
display: flex; align-items: center; gap: 8px;
152+
padding: 8px 10px; cursor: pointer; font-size: 0.95em;
153+
}
154+
.isp-option:hover { background: var(--card); }
155+
.isp-badge {
156+
display: inline-block; width: 10px; height: 10px;
157+
border-radius: 3px; flex-shrink: 0;
158+
}
134159
@media (max-width: 600px) { .form-grid { grid-template-columns: 1fr; } }
135160
</style>
136161
</head>
@@ -212,16 +237,28 @@ <h2>{{ t.mqtt_broker }} <span style="font-size:0.75em; font-weight:normal; color
212237
<h2>{{ t.general }}</h2>
213238
<div class="form-grid">
214239
<div class="form-row">
215-
<label for="isp_select">{{ t.isp_name }}</label>
216-
<div style="display:flex;align-items:center;gap:8px;">
217-
<select id="isp_select" name="isp_select" onchange="onIspChange()" style="flex:1;">
218-
<option value="">{{ t.isp_select }}</option>
240+
<label>{{ t.isp_name }}</label>
241+
<div class="isp-dropdown" id="isp-dropdown">
242+
<div class="isp-selected" id="isp-selected" onclick="toggleIspDropdown()">
243+
<span class="isp-badge" id="isp-sel-badge"></span>
244+
<span class="isp-text" id="isp-sel-text">{{ t.isp_select }}</span>
245+
<span class="isp-arrow">&#9662;</span>
246+
</div>
247+
<div class="isp-options" id="isp-options">
248+
<div class="isp-option" data-value="" onclick="selectIsp(this)">
249+
<span class="isp-text">{{ t.isp_select }}</span>
250+
</div>
219251
{% for isp in t.isp_options %}
220-
<option value="{{ isp }}" {% if config.isp_name == isp %}selected{% endif %}>{{ isp }}</option>
252+
<div class="isp-option" data-value="{{ isp }}" onclick="selectIsp(this)">
253+
{% if isp in isp_colors %}<span class="isp-badge" style="background:{{ isp_colors[isp] }}"></span>{% endif %}
254+
<span class="isp-text">{{ isp }}</span>
255+
</div>
221256
{% endfor %}
222-
<option value="__other__" {% if config.isp_name and config.isp_name not in t.isp_options %}selected{% endif %}>{{ t.isp_other }}</option>
223-
</select>
224-
<span id="isp-dot" style="width:12px;height:12px;border-radius:50%;display:none;flex-shrink:0;"></span>
257+
<div class="isp-option" data-value="__other__" onclick="selectIsp(this)">
258+
<span class="isp-text">{{ t.isp_other }}</span>
259+
</div>
260+
</div>
261+
<input type="hidden" id="isp_value" value="{{ config.isp_name or '' }}">
225262
</div>
226263
<span class="hint">{{ t.isp_hint }}</span>
227264
</div>
@@ -283,16 +320,47 @@ <h2>{{ t.general }}</h2>
283320
})();
284321

285322
var ISP_COLORS = {{ isp_colors|tojson }};
286-
function onIspChange() {
287-
var sel = document.getElementById('isp_select');
288-
var row = document.getElementById('isp-other-row');
289-
row.style.display = sel.value === '__other__' ? 'flex' : 'none';
290-
var dot = document.getElementById('isp-dot');
291-
var color = ISP_COLORS[sel.value];
292-
dot.style.display = color ? 'inline-block' : 'none';
293-
dot.style.background = color || 'transparent';
323+
var ispOpen = false;
324+
325+
function toggleIspDropdown() {
326+
ispOpen = !ispOpen;
327+
document.getElementById('isp-options').classList.toggle('open', ispOpen);
328+
}
329+
330+
function selectIsp(el) {
331+
var val = el.getAttribute('data-value');
332+
document.getElementById('isp_value').value = val;
333+
var badge = document.getElementById('isp-sel-badge');
334+
var text = document.getElementById('isp-sel-text');
335+
var color = ISP_COLORS[val];
336+
badge.style.background = color || 'transparent';
337+
badge.style.display = color ? 'inline-block' : 'none';
338+
text.textContent = el.querySelector('.isp-text').textContent;
339+
ispOpen = false;
340+
document.getElementById('isp-options').classList.remove('open');
341+
document.getElementById('isp-other-row').style.display = val === '__other__' ? 'flex' : 'none';
294342
}
295-
onIspChange();
343+
344+
document.addEventListener('click', function(e) {
345+
if (!e.target.closest('.isp-dropdown')) {
346+
ispOpen = false;
347+
document.getElementById('isp-options').classList.remove('open');
348+
}
349+
});
350+
351+
(function() {
352+
var val = document.getElementById('isp_value').value;
353+
if (!val) return;
354+
var opts = document.querySelectorAll('#isp-options .isp-option');
355+
var found = false;
356+
for (var i = 0; i < opts.length; i++) {
357+
if (opts[i].getAttribute('data-value') === val) { selectIsp(opts[i]); found = true; break; }
358+
}
359+
if (!found) {
360+
var other = document.querySelector('#isp-options .isp-option[data-value="__other__"]');
361+
selectIsp(other);
362+
}
363+
})();
296364

297365
function showToast(msg, ok) {
298366
var el = document.getElementById('toast');
@@ -308,18 +376,18 @@ <h2>{{ t.general }}</h2>
308376
function getFormData() {
309377
var form = document.getElementById('settings-form');
310378
var data = {};
311-
form.querySelectorAll('input:not(#theme-check):not(#isp_other_input), select:not(#isp_select)').forEach(function(inp) {
379+
form.querySelectorAll('input:not(#theme-check):not(#isp_other_input):not(#isp_value), select').forEach(function(inp) {
312380
if (SECRET_FIELDS.indexOf(inp.name) !== -1) {
313381
data[inp.name] = inp.value || MASK;
314382
} else {
315383
data[inp.name] = inp.value;
316384
}
317385
});
318-
var ispSel = document.getElementById('isp_select');
319-
if (ispSel.value === '__other__') {
386+
var ispVal = document.getElementById('isp_value').value;
387+
if (ispVal === '__other__') {
320388
data.isp_name = document.getElementById('isp_other_input').value;
321389
} else {
322-
data.isp_name = ispSel.value;
390+
data.isp_name = ispVal;
323391
}
324392
data.theme = themeCheck.checked ? 'dark' : 'light';
325393
return data;

app/templates/setup.html

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,31 @@
171171
.advanced-toggle .adv-hint { font-size: 0.8em; color: var(--muted); margin-left: auto; }
172172
.advanced-content { display: none; }
173173
.advanced-content.open { display: block; }
174+
.isp-dropdown { position: relative; }
175+
.isp-selected {
176+
display: flex; align-items: center; gap: 8px;
177+
background: var(--input-bg); border: 1px solid var(--input-border);
178+
border-radius: 4px; padding: 8px 10px; cursor: pointer;
179+
font-size: 0.95em; font-family: inherit; color: var(--text);
180+
}
181+
.isp-selected:hover { border-color: var(--accent); }
182+
.isp-selected .isp-arrow { margin-left: auto; font-size: 0.8em; color: var(--muted); }
183+
.isp-options {
184+
display: none; position: absolute; top: 100%; left: 0; right: 0;
185+
background: var(--input-bg); border: 1px solid var(--input-border);
186+
border-radius: 4px; margin-top: 2px; z-index: 50;
187+
max-height: 240px; overflow-y: auto;
188+
}
189+
.isp-options.open { display: block; }
190+
.isp-option {
191+
display: flex; align-items: center; gap: 8px;
192+
padding: 8px 10px; cursor: pointer; font-size: 0.95em;
193+
}
194+
.isp-option:hover { background: var(--card); }
195+
.isp-badge {
196+
display: inline-block; width: 10px; height: 10px;
197+
border-radius: 3px; flex-shrink: 0;
198+
}
174199
@media (max-width: 600px) {
175200
.form-grid { grid-template-columns: 1fr; }
176201
.advanced-toggle .adv-hint { display: none; }
@@ -225,16 +250,28 @@ <h2><span class="step-num">1</span>{{ t.modem_connection }}</h2>
225250
<h2><span class="step-num">2</span>{{ t.general }}</h2>
226251
<div class="form-grid">
227252
<div class="form-row">
228-
<label for="isp_select">{{ t.isp_name }}</label>
229-
<div style="display:flex;align-items:center;gap:8px;">
230-
<select id="isp_select" onchange="onIspChange()" style="flex:1;">
231-
<option value="">{{ t.isp_select }}</option>
253+
<label>{{ t.isp_name }}</label>
254+
<div class="isp-dropdown" id="isp-dropdown">
255+
<div class="isp-selected" id="isp-selected" onclick="toggleIspDropdown()">
256+
<span class="isp-badge" id="isp-sel-badge"></span>
257+
<span class="isp-text" id="isp-sel-text">{{ t.isp_select }}</span>
258+
<span class="isp-arrow">&#9662;</span>
259+
</div>
260+
<div class="isp-options" id="isp-options">
261+
<div class="isp-option" data-value="" onclick="selectIsp(this)">
262+
<span class="isp-text">{{ t.isp_select }}</span>
263+
</div>
232264
{% for isp in t.isp_options %}
233-
<option value="{{ isp }}" {% if config.isp_name == isp %}selected{% endif %}>{{ isp }}</option>
265+
<div class="isp-option" data-value="{{ isp }}" onclick="selectIsp(this)">
266+
{% if isp in isp_colors %}<span class="isp-badge" style="background:{{ isp_colors[isp] }}"></span>{% endif %}
267+
<span class="isp-text">{{ isp }}</span>
268+
</div>
234269
{% endfor %}
235-
<option value="__other__" {% if config.isp_name and config.isp_name not in t.isp_options %}selected{% endif %}>{{ t.isp_other }}</option>
236-
</select>
237-
<span id="isp-dot" style="width:12px;height:12px;border-radius:50%;display:none;flex-shrink:0;"></span>
270+
<div class="isp-option" data-value="__other__" onclick="selectIsp(this)">
271+
<span class="isp-text">{{ t.isp_other }}</span>
272+
</div>
273+
</div>
274+
<input type="hidden" id="isp_value" value="{{ config.isp_name or '' }}">
238275
</div>
239276
<span class="hint">{{ t.isp_hint }}</span>
240277
</div>
@@ -307,16 +344,47 @@ <h2>{{ t.mqtt_broker }}</h2>
307344
var T = {{ t|tojson }};
308345

309346
var ISP_COLORS = {{ isp_colors|tojson }};
310-
function onIspChange() {
311-
var sel = document.getElementById('isp_select');
312-
var row = document.getElementById('isp-other-row');
313-
row.style.display = sel.value === '__other__' ? 'flex' : 'none';
314-
var dot = document.getElementById('isp-dot');
315-
var color = ISP_COLORS[sel.value];
316-
dot.style.display = color ? 'inline-block' : 'none';
317-
dot.style.background = color || 'transparent';
347+
var ispOpen = false;
348+
349+
function toggleIspDropdown() {
350+
ispOpen = !ispOpen;
351+
document.getElementById('isp-options').classList.toggle('open', ispOpen);
352+
}
353+
354+
function selectIsp(el) {
355+
var val = el.getAttribute('data-value');
356+
document.getElementById('isp_value').value = val;
357+
var badge = document.getElementById('isp-sel-badge');
358+
var text = document.getElementById('isp-sel-text');
359+
var color = ISP_COLORS[val];
360+
badge.style.background = color || 'transparent';
361+
badge.style.display = color ? 'inline-block' : 'none';
362+
text.textContent = el.querySelector('.isp-text').textContent;
363+
ispOpen = false;
364+
document.getElementById('isp-options').classList.remove('open');
365+
document.getElementById('isp-other-row').style.display = val === '__other__' ? 'flex' : 'none';
318366
}
319-
onIspChange();
367+
368+
document.addEventListener('click', function(e) {
369+
if (!e.target.closest('.isp-dropdown')) {
370+
ispOpen = false;
371+
document.getElementById('isp-options').classList.remove('open');
372+
}
373+
});
374+
375+
(function() {
376+
var val = document.getElementById('isp_value').value;
377+
if (!val) return;
378+
var opts = document.querySelectorAll('#isp-options .isp-option');
379+
var found = false;
380+
for (var i = 0; i < opts.length; i++) {
381+
if (opts[i].getAttribute('data-value') === val) { selectIsp(opts[i]); found = true; break; }
382+
}
383+
if (!found) {
384+
var other = document.querySelector('#isp-options .isp-option[data-value="__other__"]');
385+
selectIsp(other);
386+
}
387+
})();
320388

321389
function toggleAdvanced() {
322390
var toggle = document.getElementById('mqtt-toggle');
@@ -339,11 +407,11 @@ <h2>{{ t.mqtt_broker }}</h2>
339407
data[inp.name] = inp.value;
340408
}
341409
});
342-
var ispSel = document.getElementById('isp_select');
343-
if (ispSel.value === '__other__') {
410+
var ispVal = document.getElementById('isp_value').value;
411+
if (ispVal === '__other__') {
344412
data.isp_name = document.getElementById('isp_other_input').value;
345413
} else {
346-
data.isp_name = ispSel.value;
414+
data.isp_name = ispVal;
347415
}
348416
data.language = '{{ lang }}';
349417
return data;

0 commit comments

Comments
 (0)