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
6 changes: 6 additions & 0 deletions src/usr/local/emhttp/plugins/fanctrlplus2/css/fcp.base.css
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@
/* Center the range separator. */
.fcp-center { text-align: center; }

/* A value under the field's floor while typing; it is raised on blur. */
#fan-form input.fcp-below-min {
outline: 2px solid #e6a23c;
outline-offset: -2px;
}

/* Mobile: full-width interval with Run Now on the next line. */
#fan-form .fcp-interval-input {
width: 100%;
Expand Down
35 changes: 30 additions & 5 deletions src/usr/local/emhttp/plugins/fanctrlplus2/fanctrlplus2.page
Original file line number Diff line number Diff line change
Expand Up @@ -643,24 +643,49 @@ $(function() {
function bindUnitInputs(unitConfigs) {
unitConfigs.forEach(({ selector, unit, min = 0, max = 100 }) => {
document.querySelectorAll(selector).forEach(input => {
const enforceUnit = () => {
// Add Fan and Add Disk Group rebind every match; only new inputs need it,
// and re-initializing one being typed in would apply its floor mid-entry.
if (input.dataset.fcpUnitBound) return;
input.dataset.fcpUnitBound = '1';
const belowMinHint = `Minimum is ${min}${unit}; a lower value is raised to it when you leave the field.`;
let ownTitle = input.title; // Some fields carry their own tooltip; put it back afterwards.
const flagBelowMin = (on) => {
input.classList.toggle('fcp-below-min', on);
if (on && input.title !== belowMinHint) {
ownTitle = input.title;
input.title = belowMinHint;
} else if (!on && input.title === belowMinHint) {
input.title = ownTitle;
}
};

// While typing, only the ceiling applies: raising a partial entry such
// as the "2" of "20" to the floor would make the next digit land on
// the floor instead (issue #17). The floor is applied on blur.
const enforceUnit = (applyMin) => {
let val = (input.value ?? '').toString().replace(/\D/g, '');

// Do not add a unit when conversion produced an empty value.
if (!val) {
input.value = '';
flagBelowMin(false);
return;
}

val = Math.min(Math.max(parseInt(val), min), max);
val = Math.min(parseInt(val, 10), max);
if (applyMin) val = Math.max(val, min);
input.value = val + unit;

// Move the cursor before the unit.
flagBelowMin(val < min);

// Move the cursor before the unit; Safari would refocus a blurred field.
if (document.activeElement !== input) return;
const pos = input.value.indexOf(unit);
input.setSelectionRange(pos, pos);
};

input.addEventListener('input', enforceUnit);
input.addEventListener('input', () => enforceUnit(false));
input.addEventListener('blur', () => enforceUnit(true));

input.addEventListener('click', () => {
setTimeout(() => {
Expand All @@ -669,7 +694,7 @@ $(function() {
}, 0);
});

enforceUnit(); // Apply once during initialization.
enforceUnit(true); // Apply once during initialization.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit floor initialization to newly added inputs

When an Add Fan or Add Disk Group AJAX request completes while the user is editing another field, the call sites at lines 521–531 and 1482–1484 invoke bindUnitInputs with selectors that also match every existing input. This unconditional initialization therefore applies the floor to the active partial value, so typing 2 toward 20 can still become 5 when the asynchronous callback runs; it can also capture the temporary minimum-warning tooltip as the field's ownTitle. Initialize only newly inserted inputs, or skip inputs that were already bound.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 5056f50. bindUnitInputs marks each input it binds and skips it on later calls, so Add Fan and Add Disk Group only initialize the new fields. That also stops the duplicate listeners. I added a test that rebinds while a field holds a partial value.

});
});
}
Expand Down
44 changes: 44 additions & 0 deletions tests/interval_ui_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,50 @@ assert.strictEqual(stripUnit('5 min'), '5');
assert.strictEqual(stripUnit('60 °C'), '60');
assert.strictEqual(stripUnit('40%'), '40');

// Typing "20" must give 20, not 50: the floor is applied when the field is
// left, never to a partial entry (issue #17).
const bindMatch = page.match(/function bindUnitInputs\(unitConfigs\) \{[\s\S]*?\n \}/);
assert(bindMatch, 'bindUnitInputs must exist');
function fakeInput() {
const handlers = {};
const classes = new Set();
return {
value: '', title: '', handlers, dataset: {},
classList: {
contains: c => classes.has(c),
remove: c => classes.delete(c),
toggle: (c, on) => (on ? classes.add(c) : classes.delete(c)),
},
addEventListener: (event, fn) => { handlers[event] = fn; },
setSelectionRange() {},
};
}
const input = fakeInput();
input.title = 'Low Temp: 40°C';
const fakeDocument = { activeElement: input, querySelectorAll: () => [input] };
const bindUnitInputs = new Function('document', `${bindMatch[0]}; return bindUnitInputs;`)(fakeDocument);
bindUnitInputs([{ selector: 'x', unit: ' sec', min: 5, max: 3600 }]);
const type = text => { input.value = text; input.handlers.input(); };

type('2');
assert.strictEqual(input.value, '2 sec', 'A partial entry below the floor is left alone');
assert(input.classList.contains('fcp-below-min'), 'A value below the floor is flagged');
assert.match(input.title, /Minimum is 5 sec/, 'A value below the floor explains the floor');
type('20 sec');
assert.strictEqual(input.value, '20 sec', 'Typing 20 gives 20, not 50');
assert(!input.classList.contains('fcp-below-min'), 'The flag clears once the floor is met');
assert.strictEqual(input.title, 'Low Temp: 40°C', "The field's own tooltip comes back once the floor is met");
type('99999');
assert.strictEqual(input.value, '3600 sec', 'The ceiling still applies while typing');
// Adding a fan or disk group rebinds every field; one mid-entry is untouched.
type('2');
bindUnitInputs([{ selector: 'x', unit: ' sec', min: 5, max: 3600 }]);
assert.strictEqual(input.value, '2 sec', 'Rebinding leaves a field being typed in alone');
type('3');
input.handlers.blur();
assert.strictEqual(input.value, '5 sec', 'Leaving the field raises the value to the floor');
assert(!input.classList.contains('fcp-below-min'), 'The flag clears after the floor is applied');

// The rendered field shows seconds, converted from whatever the config holds.
assert.match(render, /cfg_interval_seconds\(\$cfg\)/,
'The interval field must render through the shared seconds helper');
Expand Down
Loading