From 886c35bc22fb8e213a706ea49f10ab68eae13cbd Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Mon, 25 Aug 2025 00:03:45 -0400 Subject: [PATCH 01/10] Commit New Time Picker Commit New Time Picker --- MerlinAU.asp | 453 +++++++++++++++++++++------------------------------ MerlinAU.sh | 4 +- version.txt | 2 +- 3 files changed, 187 insertions(+), 272 deletions(-) diff --git a/MerlinAU.asp b/MerlinAU.asp index 89c57a3e..b1065937 100644 --- a/MerlinAU.asp +++ b/MerlinAU.asp @@ -337,38 +337,32 @@ function ToggleDaysOfWeek (isEveryXDayChecked, numberOfDays) } } -/**-------------------------------------**/ -/** Added by Martinski W. [2025-Jan-24] **/ -/**-------------------------------------**/ -function ValidateScheduleHOUR (theHOURstr) -{ - if (theHOURstr === null || - theHOURstr.length == 0 || - theHOURstr.length >= 3 || - theHOURstr.match (`${numberRegExp}`) === null) - { return false; } - let theHOURnum = parseInt(theHOURstr, 10); - if (theHOURnum < 0 || theHOURnum > 23) - { return false; } - else - { return true; } +/**---------------------------------------**/ +/** Added by ExtremeFiretop [2025-Aug-24] **/ +/**---------------------------------------**/ +function ParseTimeHHMM(v){ + v = (v || '').trim(); + if (!/^[0-2]\d:[0-5]\d$/.test(v)) return { ok:false }; + var h = parseInt(v.slice(0,2),10), m = parseInt(v.slice(3,5),10); + if (h < 0 || h > 23 || m < 0 || m > 59) return { ok:false }; + return { ok:true, h:h, m:m }; } -/**-------------------------------------**/ -/** Added by Martinski W. [2025-Jan-24] **/ -/**-------------------------------------**/ -function ValidateScheduleMINS (theMINSstr) -{ - if (theMINSstr === null || - theMINSstr.length == 0 || - theMINSstr.length >= 3 || - theMINSstr.match (`${numberRegExp}`) === null) - { return false; } - let theMINSNum = parseInt(theMINSstr, 10); - if (theMINSNum < 0 || theMINSNum > 59) - { return false; } - else - { return true; } +function ValidateTimePicker(el){ + if (!el) return false; + var res = ParseTimeHHMM(el.value); + if (res.ok){ + $(el).removeClass('Invalid'); + $(el).off('mouseover'); + return true; + }else{ + el.focus(); + $(el).addClass('Invalid'); + var msg = 'The schedule time is INVALID.
The Hour must be 0–23 and Minutes 0–59.'; + $(el).on('mouseover', function(){ return overlib(msg,0,0); }); + $(el)[0].onmouseout = nd; + return false; + } } /**-------------------------------------**/ @@ -517,216 +511,148 @@ function SetScheduleDAYofWEEK (cronDAYofWEEK) } } -/**-------------------------------------**/ -/** Added by Martinski W. [2025-Jan-24] **/ -/**-------------------------------------**/ +/**------------------------------------------**/ +/** Modified by ExtremeFiretop [2025-Aug-24] **/ +/**------------------------------------------**/ // FW_New_Update_Cron_Job_Schedule // -function FWConvertCronScheduleToWebUISettings (rawCronSchedule) -{ - let fwRawCronSched = rawCronSchedule.split(' '); - let fwScheduleHOUR = document.getElementById('fwScheduleHOUR'); - let fwScheduleMINS = document.getElementById('fwScheduleMINS'); - let fwScheduleDAYS1, fwSchedBoxDAYSX, fwScheduleXDAYS; - - if (rawCronSchedule === 'TBD' || fwRawCronSched.length < 5) - { - ToggleDaysOfWeek (true, '1'); - fwScheduleDAYS1 = document.getElementById('fwSchedBoxDAYS1'); - fwScheduleDAYS1.checked = true; - fwScheduleDAYS1.disabled = false; - fwScheduleHOUR.value = '0'; - fwScheduleMINS.value = '0'; - return; - } - let rawSchedMINS = fwRawCronSched[0]; - let rawSchedHOUR = fwRawCronSched[1]; - let rawSchedDAYM = fwRawCronSched[2]; - let rawSchedMNTH = fwRawCronSched[3]; - let rawSchedDAYW = fwRawCronSched[4]; - - if (ValidateScheduleMINS (rawSchedMINS)) - { - fwScheduleMINS.value = rawSchedMINS; - fwScheduleMINS.disabled = false; - } - else - { // Show value but DISABLED for now // - fwScheduleMINS.value = rawSchedMINS; - fwScheduleMINS.disabled = true; - } - if (ValidateScheduleHOUR (rawSchedHOUR)) - { - fwScheduleHOUR.value = rawSchedHOUR; - fwScheduleHOUR.disabled = false; - } - else - { // Show value but DISABLED for now // - fwScheduleHOUR.value = rawSchedHOUR; - fwScheduleHOUR.disabled = true; - } - if (rawSchedDAYM.match ('[*]/([2-9]|1[0-5])') !== null) - { - ToggleDaysOfWeek (true, 'X'); - fwSchedBoxDAYSX = document.getElementById('fwSchedBoxDAYSX'); - fwSchedBoxDAYSX.checked = true; - fwSchedBoxDAYSX.disabled = false; - fwScheduleXDAYS = document.getElementById('fwScheduleXDAYS'); - let tempArray = rawSchedDAYM.split('/'); - if (tempArray.length > 1) - { fwScheduleXDAYS.value = tempArray[1]; } - return; - } - else if (rawSchedDAYW.match ('[*]/[2-3]') !== null) - { - ToggleDaysOfWeek (true, 'X'); - fwSchedBoxDAYSX = document.getElementById('fwSchedBoxDAYSX'); - fwSchedBoxDAYSX.checked = true; - fwSchedBoxDAYSX.disabled = false; - fwScheduleXDAYS = document.getElementById('fwScheduleXDAYS'); - let tempArray = rawSchedDAYW.split('/'); - if (tempArray.length > 1) - { fwScheduleXDAYS.value = tempArray[1]; } - return; - } - else if (rawSchedDAYM === '*' && rawSchedDAYW === '*') - { - ToggleDaysOfWeek (true, '1'); - fwScheduleDAYS1 = document.getElementById('fwSchedBoxDAYS1'); - fwScheduleDAYS1.checked = true; - fwScheduleDAYS1.disabled = false; - return; - } - else if (rawSchedDAYM != '*' || rawSchedMNTH != '*') - { /**------------------------------------------------------**/ - /** We do NOT yet handle schedules with different Months **/ - /** or intervals, lists or ranges of "Days of the Month" **/ - /**------------------------------------------------------**/ - // Toggle OFF checkboxes for now // - ToggleDaysOfWeek (true, '0'); - return; - } - if (ValidateScheduleDAYofWEEK (rawSchedDAYW)) - { SetScheduleDAYofWEEK (rawSchedDAYW); } +function FWConvertCronScheduleToWebUISettings(rawCronSchedule){ + let fwRaw = rawCronSchedule.split(' '); + let T = document.getElementById('fwScheduleTIME'); + let fwSchedD1, fwSchedX, fwXD; + + if (!T) return; + + if (rawCronSchedule === 'TBD' || fwRaw.length < 5){ + ToggleDaysOfWeek(true, '1'); + fwSchedD1 = document.getElementById('fwSchedBoxDAYS1'); + if (fwSchedD1){ fwSchedD1.checked = true; fwSchedD1.disabled = false; } + T.value = '00:00'; + T.disabled = false; + return; + } + + let rawM = fwRaw[0], rawH = fwRaw[1], rawDM = fwRaw[2], rawMN = fwRaw[3], rawDW = fwRaw[4]; + + // Set the time picker value (even if later disabled) + let h = parseInt(rawH,10), m = parseInt(rawM,10); + if (isNaN(h) || h < 0 || h > 23) h = 0; + if (isNaN(m) || m < 0 || m > 59) m = 0; + var hh = (h < 10 ? '0' : '') + h; + var mm = (m < 10 ? '0' : '') + m; + T.value = hh + ':' + mm; + + // Days logic — toggle the time input disabledness + if (rawDM.match ('[*]/([2-9]|1[0-5])') !== null){ + ToggleDaysOfWeek(true, 'X'); + fwSchedX = document.getElementById('fwSchedBoxDAYSX'); + if (fwSchedX){ fwSchedX.checked = true; fwSchedX.disabled = false; } + fwXD = document.getElementById('fwScheduleXDAYS'); + let temp = rawDM.split('/'); + if (fwXD && temp.length > 1){ fwXD.value = temp[1]; } + T.disabled = false; // allow time selection + return; + } + else if (rawDW.match ('[*]/[2-3]') !== null){ + ToggleDaysOfWeek(true, 'X'); + fwSchedX = document.getElementById('fwSchedBoxDAYSX'); + if (fwSchedX){ fwSchedX.checked = true; fwSchedX.disabled = false; } + fwXD = document.getElementById('fwScheduleXDAYS'); + let temp = rawDW.split('/'); + if (fwXD && temp.length > 1){ fwXD.value = temp[1]; } + T.disabled = false; + return; + } + else if (rawDM === '*' && rawDW === '*'){ + ToggleDaysOfWeek(true, '1'); + fwSchedD1 = document.getElementById('fwSchedBoxDAYS1'); + if (fwSchedD1){ fwSchedD1.checked = true; fwSchedD1.disabled = false; } + T.disabled = false; + return; + } + else if (rawDM != '*' || rawMN != '*'){ + // Not handled in UI (DoM/Month intervals) — mirror prior behavior + ToggleDaysOfWeek(true, '0'); // disable day checkboxes + T.disabled = false; // still allow time + return; + } + + if (ValidateScheduleDAYofWEEK(rawDW)){ SetScheduleDAYofWEEK(rawDW); } + T.disabled = false; } -/**-------------------------------------**/ -/** Added by Martinski W. [2025-Jan-25] **/ -/**-------------------------------------**/ -function FWConvertWebUISettingsToCronSchedule (oldRawCronSchedule) -{ - let newRawCronSchedule = ''; - let theFWRawCronSched = oldRawCronSchedule.split(' '); - - //Temporary delimiter is replaced later by shell script// - const delimChar = '|'; - const defaultSchedule = '0|0|*|*|*'; - - if (oldRawCronSchedule === 'TBD' || theFWRawCronSched.length < 5) - { //Default CRON Schedule// - newRawCronSchedule = defaultSchedule; - return (newRawCronSchedule); - } - - let fwRawSchedMINS = theFWRawCronSched[0]; - let fwRawSchedHOUR = theFWRawCronSched[1]; - let fwRawSchedDAYM = theFWRawCronSched[2]; - let fwRawSchedMNTH = theFWRawCronSched[3]; - let fwRawSchedDAYW = theFWRawCronSched[4]; - let fwScheduleHOUR = document.getElementById('fwScheduleHOUR'); - let fwScheduleMINS = document.getElementById('fwScheduleMINS'); - let fwScheduleDAYS1 = document.getElementById('fwSchedBoxDAYS1'); - let fwSchedBoxDAYSX = document.getElementById('fwSchedBoxDAYSX'); - let fwScheduleXDAYS = document.getElementById('fwScheduleXDAYS'); - let fwScheduleMON = document.getElementById('fwSched_MON'); - let fwScheduleTUE = document.getElementById('fwSched_TUE'); - let fwScheduleWED = document.getElementById('fwSched_WED'); - let fwScheduleTHU = document.getElementById('fwSched_THU'); - let fwScheduleFRI = document.getElementById('fwSched_FRI'); - let fwScheduleSAT = document.getElementById('fwSched_SAT'); - let fwScheduleSUN = document.getElementById('fwSched_SUN'); - - if (fwScheduleMINS.disabled === false) - { fwRawSchedMINS = fwScheduleMINS.value; } - - if (fwScheduleHOUR.disabled === false) - { fwRawSchedHOUR = fwScheduleHOUR.value; } - - if (fwScheduleDAYS1.checked && - fwScheduleDAYS1.disabled === false) - { - fwRawSchedDAYM = '*'; - fwRawSchedDAYW = '*'; - } - else if (fwSchedBoxDAYSX.checked && - fwSchedBoxDAYSX.disabled === false) - { - if (fwRawSchedDAYW.match ('[*]/[2-3]') !== null && - (fwScheduleXDAYS.value == 2 || fwScheduleXDAYS.value == 3)) - { - fwRawSchedDAYM = '*'; - fwRawSchedDAYW = '*/' + fwScheduleXDAYS.value; - } - else - { - fwRawSchedDAYW = '*'; - fwRawSchedDAYM = '*/' + fwScheduleXDAYS.value; - } - } - else - { - let daysOfWeekArray = [], daysOfWeekIndex = []; - if (fwScheduleSUN.checked && fwScheduleSUN.disabled === false) - { - daysOfWeekIndex.push(0); - daysOfWeekArray.push('Sun'); - } - if (fwScheduleMON.checked && fwScheduleMON.disabled === false) - { - daysOfWeekIndex.push(1); - daysOfWeekArray.push('Mon'); - } - if (fwScheduleTUE.checked && fwScheduleTUE.disabled === false) - { - daysOfWeekIndex.push(2); - daysOfWeekArray.push('Tue'); - } - if (fwScheduleWED.checked && fwScheduleWED.disabled === false) - { - daysOfWeekIndex.push(3); - daysOfWeekArray.push('Wed'); - } - if (fwScheduleTHU.checked && fwScheduleTHU.disabled === false) - { - daysOfWeekIndex.push(4); - daysOfWeekArray.push('Thu'); - } - if (fwScheduleFRI.checked && fwScheduleFRI.disabled === false) - { - daysOfWeekIndex.push(5); - daysOfWeekArray.push('Fri'); - } - if (fwScheduleSAT.checked && fwScheduleSAT.disabled === false) - { - daysOfWeekIndex.push(6); - daysOfWeekArray.push('Sat'); - } - if (daysOfWeekArray.length > 0) - { - fwRawSchedDAYM = '*'; - fwRawSchedMNTH = '*'; - if (daysOfWeekArray.length == 7) - { fwRawSchedDAYW = '*'; } - else - { fwRawSchedDAYW = GetCronDAYofWEEK (daysOfWeekIndex, daysOfWeekArray); } - } - } - newRawCronSchedule = fwRawSchedMINS + delimChar + - fwRawSchedHOUR + delimChar + - fwRawSchedDAYM + delimChar + - fwRawSchedMNTH + delimChar + - fwRawSchedDAYW; +/**------------------------------------------**/ +/** Modified by ExtremeFiretop [2025-Aug-24] **/ +/**------------------------------------------**/ +function FWConvertWebUISettingsToCronSchedule(oldRawCronSchedule){ + const delimChar = '|'; + const defaultSchedule = '0|0|*|*|*'; + + if (oldRawCronSchedule === 'TBD' || oldRawCronSchedule.split(' ').length < 5){ + return defaultSchedule; + } + + let fwRawSchedMINS = '0'; + let fwRawSchedHOUR = '0'; + let fwRawSchedDAYM = '*'; + let fwRawSchedMNTH = '*'; + let fwRawSchedDAYW = '*'; + + let T = document.getElementById('fwScheduleTIME'); + + // time from the single control + let parsed = ParseTimeHHMM(T ? T.value : ''); + if (parsed.ok){ + fwRawSchedHOUR = String(parsed.h); + fwRawSchedMINS = String(parsed.m); + } + + // days logic (unchanged) + let fwScheduleDAYS1 = document.getElementById('fwSchedBoxDAYS1'); + let fwSchedBoxDAYSX = document.getElementById('fwSchedBoxDAYSX'); + let fwScheduleXDAYS = document.getElementById('fwScheduleXDAYS'); + let fwScheduleMON = document.getElementById('fwSched_MON'); + let fwScheduleTUE = document.getElementById('fwSched_TUE'); + let fwScheduleWED = document.getElementById('fwSched_WED'); + let fwScheduleTHU = document.getElementById('fwSched_THU'); + let fwScheduleFRI = document.getElementById('fwSched_FRI'); + let fwScheduleSAT = document.getElementById('fwSched_SAT'); + let fwScheduleSUN = document.getElementById('fwSched_SUN'); + + if (fwScheduleDAYS1 && fwScheduleDAYS1.checked && fwScheduleDAYS1.disabled === false){ + fwRawSchedDAYM = '*'; + fwRawSchedDAYW = '*'; + } + else if (fwSchedBoxDAYSX && fwSchedBoxDAYSX.checked && fwSchedBoxDAYSX.disabled === false){ + if ((fwScheduleXDAYS.value == 2 || fwScheduleXDAYS.value == 3)){ + fwRawSchedDAYM = '*'; + fwRawSchedDAYW = '*/' + fwScheduleXDAYS.value; + } else { + fwRawSchedDAYW = '*'; + fwRawSchedDAYM = '*/' + fwScheduleXDAYS.value; + } + } + else { + let daysOfWeekArray = [], daysOfWeekIndex = []; + if (fwScheduleSUN && fwScheduleSUN.checked && fwScheduleSUN.disabled === false){ daysOfWeekIndex.push(0); daysOfWeekArray.push('Sun'); } + if (fwScheduleMON && fwScheduleMON.checked && fwScheduleMON.disabled === false){ daysOfWeekIndex.push(1); daysOfWeekArray.push('Mon'); } + if (fwScheduleTUE && fwScheduleTUE.checked && fwScheduleTUE.disabled === false){ daysOfWeekIndex.push(2); daysOfWeekArray.push('Tue'); } + if (fwScheduleWED && fwScheduleWED.checked && fwScheduleWED.disabled === false){ daysOfWeekIndex.push(3); daysOfWeekArray.push('Wed'); } + if (fwScheduleTHU && fwScheduleTHU.checked && fwScheduleTHU.disabled === false){ daysOfWeekIndex.push(4); daysOfWeekArray.push('Thu'); } + if (fwScheduleFRI && fwScheduleFRI.checked && fwScheduleFRI.disabled === false){ daysOfWeekIndex.push(5); daysOfWeekArray.push('Fri'); } + if (fwScheduleSAT && fwScheduleSAT.checked && fwScheduleSAT.disabled === false){ daysOfWeekIndex.push(6); daysOfWeekArray.push('Sat'); } + + if (daysOfWeekArray.length > 0){ + fwRawSchedDAYM = '*'; + fwRawSchedMNTH = '*'; + fwRawSchedDAYW = (daysOfWeekArray.length == 7) ? '*' : GetCronDAYofWEEK(daysOfWeekIndex, daysOfWeekArray); + } + } - return (newRawCronSchedule); + return fwRawSchedMINS + delimChar + + fwRawSchedHOUR + delimChar + + fwRawSchedDAYM + delimChar + + fwRawSchedMNTH + delimChar + + fwRawSchedDAYW; } /**----------------------------------------**/ @@ -2139,9 +2065,9 @@ function initial() } } -/**----------------------------------------**/ -/** Modified by Martinski W. [2025-Mar-07] **/ -/**----------------------------------------**/ +/**------------------------------------------**/ +/** Modified by ExtremeFiretop [2025-Aug-24] **/ +/**------------------------------------------**/ function SaveCombinedConfig() { // Clear the hidden field before saving // @@ -2170,17 +2096,10 @@ function SaveCombinedConfig() alert(`${validationErrorMsg}\n\n` + fwPostponedDays.ErrorMsg()); return false; } - if (document.form.fwScheduleHOUR.disabled === false && - !ValidateFWUpdateTime(document.form.fwScheduleHOUR, 'HOUR')) - { - alert(`${validationErrorMsg}\n\n` + fwScheduleTime.ErrorMsg('HOUR')); - return false; - } - if (document.form.fwScheduleMINS.disabled === false && - !ValidateFWUpdateTime(document.form.fwScheduleMINS, 'MINS')) - { - alert(`${validationErrorMsg}\n\n` + fwScheduleTime.ErrorMsg('MINS')); - return false; + var timeEl = document.getElementById('fwScheduleTIME'); + if (timeEl && timeEl.disabled === false && !ValidateTimePicker(timeEl)){ + alert(validationErrorMsg + '\n\nThe schedule time is INVALID.\nThe Hour must be between 0 and 23, and Minutes between 0 and 59.'); + return false; } if (document.getElementById('fwSchedBoxDAYSX').checked && !ValidateFWUpdateXDays(document.form.fwScheduleXDAYS, 'DAYS')) @@ -2638,7 +2557,7 @@ function initializeCollapsibleSections()
MerlinAU
This is the MerlinAU add-on integrated into the router WebUI -[ +[ Wiki ] @@ -2893,7 +2812,7 @@ function initializeCollapsibleSections() -
+
Days:
-
- Hour: - -
-
- Minutes: - + +
+ Time: +
diff --git a/MerlinAU.sh b/MerlinAU.sh index 49f5abe9..3edf9d68 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -9,8 +9,8 @@ set -u ## Set version for each Production Release ## -readonly SCRIPT_VERSION=1.5.3 -readonly SCRIPT_VERSTAG="25081319" +readonly SCRIPT_VERSION=1.5.4 +readonly SCRIPT_VERSTAG="25082423" readonly SCRIPT_NAME="MerlinAU" ## Set to "master" for Production Releases ## SCRIPT_BRANCH="dev" diff --git a/version.txt b/version.txt index 8af85beb..94fe62c2 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.5.3 +1.5.4 From be867192cf597b50d279104cf18d9c06eac60cca Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Mon, 25 Aug 2025 08:47:03 -0400 Subject: [PATCH 02/10] Fix Input Validator Fix Input Validator --- MerlinAU.asp | 115 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 22 deletions(-) diff --git a/MerlinAU.asp b/MerlinAU.asp index b1065937..d96ebb31 100644 --- a/MerlinAU.asp +++ b/MerlinAU.asp @@ -39,6 +39,8 @@ var shared_custom_settings = {}; var ajax_custom_settings = {}; let isFormSubmitting = false; let FW_NewUpdateVersAvailable = ''; +var fwTimeInvalidFromConfig = false; +var fwTimeInvalidMsg = ''; // Order of NVRAM keys to search for 'Model ID' and 'Product ID' // const modelKeys = ["nvram_odmpid", "nvram_wps_modelnum", "nvram_model", "nvram_build_name"]; @@ -348,19 +350,67 @@ function ParseTimeHHMM(v){ return { ok:true, h:h, m:m }; } +function ValidateHHMMUsingFwScheduleTime(hhmm){ + var v = (hhmm || '').trim(); + var parts = v.split(':'); + if (parts.length !== 2){ + return { + ok: false, + msg: fwScheduleTime.ErrorMsgHOUR() + '\n' + fwScheduleTime.ErrorMsgMINS() + }; + } + var H = parts[0], M = parts[1]; + var hOk = fwScheduleTime.ValidateHOUR(H); + var mOk = fwScheduleTime.ValidateMINS(M); + var msg = ''; + if (!hOk) msg += fwScheduleTime.ErrorMsgHOUR(); + if (!mOk) msg += (msg ? '\n' : '') + fwScheduleTime.ErrorMsgMINS(); + return { ok: (hOk && mOk), msg: msg }; +} + +function MarkTimePickerInvalid(T, msg){ + if (!T) return; + fwTimeInvalidFromConfig = true; + fwTimeInvalidMsg = msg; + + var $t = $(T); + $t.addClass('Invalid'); + // Remove ONLY namespaced handlers, then rebind + $t.off('.fwtime'); + // Show tooltip ONLY on mouseover (no focus) + $t.on('mouseover.fwtime', function(){ return overlib(msg,0,0); }); + // Hide tooltip as soon as user stops hovering or edits/leaves the field + $t.on('mouseleave.fwtime input.fwtime keydown.fwtime keyup.fwtime blur.fwtime', function(){ + try { nd(); } catch(e){} + }); + // if something already opened a tooltip, close it now + try { nd(); } catch(e){} + T.setAttribute('aria-invalid','true'); + T.value = ''; + setTimeout(function(){ T.focus(); }, 0); // focus no longer triggers tooltip +} + +function ClearTimePickerInvalid(T){ + if (!T) return; + fwTimeInvalidFromConfig = false; + fwTimeInvalidMsg = ''; + var $t = $(T); + $t.removeClass('Invalid'); + $t.off('.fwtime'); // remove our handlers + try { nd(); } catch(e){} // force-close any lingering overlib + T.removeAttribute('aria-invalid'); +} + function ValidateTimePicker(el){ if (!el) return false; - var res = ParseTimeHHMM(el.value); - if (res.ok){ - $(el).removeClass('Invalid'); - $(el).off('mouseover'); + var r = ValidateHHMMUsingFwScheduleTime(el.value); + if (r.ok){ + try { nd(); } catch(e){} + ClearTimePickerInvalid(el); return true; }else{ - el.focus(); - $(el).addClass('Invalid'); - var msg = 'The schedule time is INVALID.
The Hour must be 0–23 and Minutes 0–59.'; - $(el).on('mouseover', function(){ return overlib(msg,0,0); }); - $(el)[0].onmouseout = nd; + // keep newline->
for overlib formatting + MarkTimePickerInvalid(el, (r.msg || '').replace(/\n/g,'
')); return false; } } @@ -522,6 +572,9 @@ function FWConvertCronScheduleToWebUISettings(rawCronSchedule){ if (!T) return; + // default UI state + ClearTimePickerInvalid(T); + if (rawCronSchedule === 'TBD' || fwRaw.length < 5){ ToggleDaysOfWeek(true, '1'); fwSchedD1 = document.getElementById('fwSchedBoxDAYS1'); @@ -533,15 +586,19 @@ function FWConvertCronScheduleToWebUISettings(rawCronSchedule){ let rawM = fwRaw[0], rawH = fwRaw[1], rawDM = fwRaw[2], rawMN = fwRaw[3], rawDW = fwRaw[4]; - // Set the time picker value (even if later disabled) - let h = parseInt(rawH,10), m = parseInt(rawM,10); - if (isNaN(h) || h < 0 || h > 23) h = 0; - if (isNaN(m) || m < 0 || m > 59) m = 0; - var hh = (h < 10 ? '0' : '') + h; - var mm = (m < 10 ? '0' : '') + m; - T.value = hh + ':' + mm; + // ---- Time handling: use fwScheduleTime messages only ---- + let timeCheck = ValidateHHMMUsingFwScheduleTime(String(rawH) + ':' + String(rawM)); + if (!timeCheck.ok){ + MarkTimePickerInvalid(T, (timeCheck.msg || '').replace(/\n/g,'
')); + } else { + let h = parseInt(rawH,10), m = parseInt(rawM,10); + var hh = (h < 10 ? '0' : '') + h; + var mm = (m < 10 ? '0' : '') + m; + T.value = hh + ':' + mm; + ClearTimePickerInvalid(T); + } - // Days logic — toggle the time input disabledness + // Days logic if (rawDM.match ('[*]/([2-9]|1[0-5])') !== null){ ToggleDaysOfWeek(true, 'X'); fwSchedX = document.getElementById('fwSchedBoxDAYSX'); @@ -549,7 +606,7 @@ function FWConvertCronScheduleToWebUISettings(rawCronSchedule){ fwXD = document.getElementById('fwScheduleXDAYS'); let temp = rawDM.split('/'); if (fwXD && temp.length > 1){ fwXD.value = temp[1]; } - T.disabled = false; // allow time selection + T.disabled = false; return; } else if (rawDW.match ('[*]/[2-3]') !== null){ @@ -570,9 +627,9 @@ function FWConvertCronScheduleToWebUISettings(rawCronSchedule){ return; } else if (rawDM != '*' || rawMN != '*'){ - // Not handled in UI (DoM/Month intervals) — mirror prior behavior + // Not handled in UI (DoM/Month intervals) ToggleDaysOfWeek(true, '0'); // disable day checkboxes - T.disabled = false; // still allow time + T.disabled = false; return; } @@ -2096,11 +2153,25 @@ function SaveCombinedConfig() alert(`${validationErrorMsg}\n\n` + fwPostponedDays.ErrorMsg()); return false; } + // ---- Time validation: also catch invalid time loaded from settings ---- var timeEl = document.getElementById('fwScheduleTIME'); - if (timeEl && timeEl.disabled === false && !ValidateTimePicker(timeEl)){ - alert(validationErrorMsg + '\n\nThe schedule time is INVALID.\nThe Hour must be between 0 and 23, and Minutes between 0 and 59.'); + + // If loader marked it invalid + if (fwTimeInvalidFromConfig){ + alert(validationErrorMsg + '\n\n' + fwTimeInvalidMsg.replace(/
/g, '\n')); + if (timeEl) timeEl.focus(); return false; } + + // Normal validation of the control’s current value + if (timeEl && timeEl.disabled === false){ + var chk = ValidateHHMMUsingFwScheduleTime(timeEl.value); + if (!chk.ok){ + alert(validationErrorMsg + '\n\n' + chk.msg); + timeEl.focus(); + return false; + } + } if (document.getElementById('fwSchedBoxDAYSX').checked && !ValidateFWUpdateXDays(document.form.fwScheduleXDAYS, 'DAYS')) { From 760e9792e492998d1a0571531eceb8ff5b3addca Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Mon, 25 Aug 2025 09:31:49 -0400 Subject: [PATCH 03/10] Standardize the Error Message Standardize the Error Message --- MerlinAU.asp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/MerlinAU.asp b/MerlinAU.asp index d96ebb31..f554db0b 100644 --- a/MerlinAU.asp +++ b/MerlinAU.asp @@ -353,19 +353,17 @@ function ParseTimeHHMM(v){ function ValidateHHMMUsingFwScheduleTime(hhmm){ var v = (hhmm || '').trim(); var parts = v.split(':'); + // One unified message for ANY invalid time input + var commonMsg = fwScheduleTime.ErrorMsgHOUR() + '\n' + fwScheduleTime.ErrorMsgMINS(); + if (parts.length !== 2){ - return { - ok: false, - msg: fwScheduleTime.ErrorMsgHOUR() + '\n' + fwScheduleTime.ErrorMsgMINS() - }; + return { ok: false, msg: commonMsg }; } var H = parts[0], M = parts[1]; var hOk = fwScheduleTime.ValidateHOUR(H); var mOk = fwScheduleTime.ValidateMINS(M); - var msg = ''; - if (!hOk) msg += fwScheduleTime.ErrorMsgHOUR(); - if (!mOk) msg += (msg ? '\n' : '') + fwScheduleTime.ErrorMsgMINS(); - return { ok: (hOk && mOk), msg: msg }; + // Always return the same message when invalid; no hour/minute-specific messaging + return { ok: (hOk && mOk), msg: (hOk && mOk) ? '' : commonMsg }; } function MarkTimePickerInvalid(T, msg){ From 47f45b6fce1c046de2679c07c26f912428766701 Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Mon, 25 Aug 2025 10:59:27 -0400 Subject: [PATCH 04/10] Fix Production Issue Fix Production Issue --- MerlinAU.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/MerlinAU.sh b/MerlinAU.sh index 3edf9d68..243e0d48 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -2529,6 +2529,15 @@ _ActionsAfterNewConfigSettings_() then _Calculate_NextRunTime_ recal fi + if _ConfigOptionChanged_ "FW_New_Update_Cron_Job_Schedule=" + then + newSchedule="$(Get_Custom_Setting FW_New_Update_Cron_Job_Schedule)" + if _AddFWAutoUpdateCronJob_ "$newSchedule" + then + Say "Cron Job [$newSchedule] was updated successfully." + _Calculate_NextRunTime_ recal + fi + fi if _ConfigOptionChanged_ "Allow_Script_Auto_Update" then ScriptAutoUpdateSetting="$(Get_Custom_Setting Allow_Script_Auto_Update)" From cfd727af203626496cba8516f6b93704e11c2eaa Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Mon, 25 Aug 2025 11:00:36 -0400 Subject: [PATCH 05/10] Remove Text Remove Text --- MerlinAU.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 243e0d48..ffee4242 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -2534,7 +2534,6 @@ _ActionsAfterNewConfigSettings_() newSchedule="$(Get_Custom_Setting FW_New_Update_Cron_Job_Schedule)" if _AddFWAutoUpdateCronJob_ "$newSchedule" then - Say "Cron Job [$newSchedule] was updated successfully." _Calculate_NextRunTime_ recal fi fi From 4ddd06a66b57747b96366369acd97779794a9b74 Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Tue, 2 Sep 2025 00:15:31 -0400 Subject: [PATCH 06/10] Firefox Fallback Method Firefox Fallback Method --- MerlinAU.asp | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/MerlinAU.asp b/MerlinAU.asp index f554db0b..cbe2d376 100644 --- a/MerlinAU.asp +++ b/MerlinAU.asp @@ -339,6 +339,49 @@ function ToggleDaysOfWeek (isEveryXDayChecked, numberOfDays) } } +function MerlinAU_TimeSelectFallbackAttach(id) { + var input = document.getElementById(id); + if (!input) return; + + var isFirefox = /firefox/i.test(navigator.userAgent); + if (!isFirefox && typeof input.showPicker === 'function') return; + + function pad(n){ return (n<10?'0':'')+n; } + function parseHHMM(v){ + v = (v||'').trim(); + var m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(v); + if (m) return {h:+m[1], m:+m[2]}; + var d = new Date(); return {h:d.getHours(), m:d.getMinutes()}; + } + + var wrap = document.createElement('span'); + var selH = document.createElement('select'); + var selM = document.createElement('select'); + + for (var h=0; h<24; h++) selH.add(new Option(pad(h), h)); + for (var m=0; m<60; m++) selM.add(new Option(pad(m), m)); + + input.readOnly = true; + + function apply(){ + var hh = pad(+selH.value), mm = pad(+selM.value); + input.value = hh + ':' + mm; + if (window.ClearTimePickerInvalid) window.ClearTimePickerInvalid(input); + if (window.ValidateTimePicker) window.ValidateTimePicker(input); + } + + if (input.nextSibling) input.parentNode.insertBefore(wrap, input.nextSibling); + else input.parentNode.appendChild(wrap); + wrap.appendChild(selH); + wrap.appendChild(document.createTextNode(' : ')); + wrap.appendChild(selM); + + var start = parseHHMM(input.value); + selH.value = start.h; selM.value = start.m; + selH.onchange = apply; selM.onchange = apply; + apply(); // normalize displayed HH:MM +} + /**---------------------------------------**/ /** Added by ExtremeFiretop [2025-Aug-24] **/ /**---------------------------------------**/ @@ -2107,6 +2150,7 @@ function initial() UpdateScriptVersion(); showhide('Script_AutoUpdate_SchedText',true); showhide('FW_AutoUpdate_CheckSchedText',true); + MerlinAU_TimeSelectFallbackAttach('fwScheduleTIME'); // Debugging iframe behavior // var hiddenFrame = document.getElementById('hidden_frame'); @@ -2929,7 +2973,7 @@ function initializeCollapsibleSections() name="fwScheduleTIME" value="00:00" step="60" - style="width: 140px; margin-left: 18px; margin-top: 6px; margin-bottom: 10px;" + style="width: 120px; margin-left: 18px; margin-top: 6px; margin-bottom: 10px;" oninput="ValidateTimePicker(this)" onblur="ValidateTimePicker(this)" /> From f2d051a48126d073361a428346df29d9c6e1d87a Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Tue, 2 Sep 2025 00:44:43 -0400 Subject: [PATCH 07/10] Bug Fixes, Hardening Solution Bug Fixes, Hardening Solution --- MerlinAU.asp | 94 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/MerlinAU.asp b/MerlinAU.asp index cbe2d376..bc148099 100644 --- a/MerlinAU.asp +++ b/MerlinAU.asp @@ -41,6 +41,7 @@ let isFormSubmitting = false; let FW_NewUpdateVersAvailable = ''; var fwTimeInvalidFromConfig = false; var fwTimeInvalidMsg = ''; +var fwUpdateEstimatedRunDate = 'TBD'; // Order of NVRAM keys to search for 'Model ID' and 'Product ID' // const modelKeys = ["nvram_odmpid", "nvram_wps_modelnum", "nvram_model", "nvram_build_name"]; @@ -343,8 +344,16 @@ function MerlinAU_TimeSelectFallbackAttach(id) { var input = document.getElementById(id); if (!input) return; + // guard: don’t double-attach + if (input.dataset.mauTimeApplied === '1') return; + + // Native is fine almost everywhere except Firefox. var isFirefox = /firefox/i.test(navigator.userAgent); - if (!isFirefox && typeof input.showPicker === 'function') return; + var probe = document.createElement('input'); + probe.type = 'time'; + var hasNative = (probe.type === 'time'); // real time input, not text + + if (hasNative && !isFirefox) return; // keep native on Chrome/Edge/Safari/iOS/Android function pad(n){ return (n<10?'0':'')+n; } function parseHHMM(v){ @@ -355,19 +364,29 @@ function MerlinAU_TimeSelectFallbackAttach(id) { } var wrap = document.createElement('span'); + wrap.className = 'mau-time-fallback'; var selH = document.createElement('select'); var selM = document.createElement('select'); for (var h=0; h<24; h++) selH.add(new Option(pad(h), h)); - for (var m=0; m<60; m++) selM.add(new Option(pad(m), m)); + var stepSec = parseInt(input.getAttribute('step') || '60', 10); + if (isNaN(stepSec) || stepSec < 60) stepSec = 60; + var minInc = Math.max(1, Math.floor(stepSec/60)); + for (var m=0; m<60; m+=minInc) selM.add(new Option(pad(m), m)); input.readOnly = true; + input.dataset.mauTimeApplied = '1'; + + function emit(el, type){ + el.dispatchEvent(new Event(type, { bubbles:true })); + } function apply(){ var hh = pad(+selH.value), mm = pad(+selM.value); input.value = hh + ':' + mm; if (window.ClearTimePickerInvalid) window.ClearTimePickerInvalid(input); if (window.ValidateTimePicker) window.ValidateTimePicker(input); + emit(input,'input'); emit(input,'change'); } if (input.nextSibling) input.parentNode.insertBefore(wrap, input.nextSibling); @@ -376,10 +395,40 @@ function MerlinAU_TimeSelectFallbackAttach(id) { wrap.appendChild(document.createTextNode(' : ')); wrap.appendChild(selM); + input.setAttribute('aria-hidden','true'); + input.tabIndex = -1; + selH.setAttribute('aria-label','Hour'); + selM.setAttribute('aria-label','Minute'); + var start = parseHHMM(input.value); selH.value = start.h; selM.value = start.m; selH.onchange = apply; selM.onchange = apply; - apply(); // normalize displayed HH:MM + var flaggedInvalid = (input.getAttribute('aria-invalid') === 'true') || + (typeof fwTimeInvalidFromConfig !== 'undefined' && fwTimeInvalidFromConfig) || + (input.classList && input.classList.contains('Invalid')); + + if (!flaggedInvalid) { + apply(); // normalize displayed HH:MM + } + + function syncDisabled(){ + var dis = !!input.disabled; + selH.disabled = dis; selM.disabled = dis; + wrap.style.opacity = dis ? '0.5' : '1'; + } + syncDisabled(); + + var mo = new MutationObserver(syncDisabled); + mo.observe(input, { attributes:true, attributeFilter:['disabled'] }); + + function syncFromInput(){ + var v = (input.value || '').trim(); + if (!v) return; // keep current selects if input was cleared for invalid state + var t = parseHHMM(v); + selH.value = t.h; selM.value = t.m; + } + input.addEventListener('input', syncFromInput); + input.addEventListener('change', syncFromInput); } /**---------------------------------------**/ @@ -415,20 +464,21 @@ function MarkTimePickerInvalid(T, msg){ fwTimeInvalidMsg = msg; var $t = $(T); - $t.addClass('Invalid'); - // Remove ONLY namespaced handlers, then rebind - $t.off('.fwtime'); - // Show tooltip ONLY on mouseover (no focus) - $t.on('mouseover.fwtime', function(){ return overlib(msg,0,0); }); - // Hide tooltip as soon as user stops hovering or edits/leaves the field - $t.on('mouseleave.fwtime input.fwtime keydown.fwtime keyup.fwtime blur.fwtime', function(){ - try { nd(); } catch(e){} - }); - // if something already opened a tooltip, close it now + $t.addClass('Invalid').off('.fwtime') + .on('mouseover.fwtime', function(){ return overlib(msg,0,0); }) + .on('mouseleave.fwtime input.fwtime keydown.fwtime keyup.fwtime blur.fwtime', function(){ try { nd(); } catch(e){} }); + try { nd(); } catch(e){} T.setAttribute('aria-invalid','true'); T.value = ''; - setTimeout(function(){ T.focus(); }, 0); // focus no longer triggers tooltip + + // If our fallback is attached, focus the hour select instead of the hidden input + if (T.dataset && T.dataset.mauTimeApplied === '1') { + var wrap = T.nextElementSibling; // our wrapper + var sel = wrap && wrap.querySelector('select'); + if (sel) { sel.focus(); return; } + } + setTimeout(function(){ T.focus(); }, 0); } function ClearTimePickerInvalid(T){ @@ -607,25 +657,22 @@ function SetScheduleDAYofWEEK (cronDAYofWEEK) /**------------------------------------------**/ // FW_New_Update_Cron_Job_Schedule // function FWConvertCronScheduleToWebUISettings(rawCronSchedule){ - let fwRaw = rawCronSchedule.split(' '); + let s = (rawCronSchedule || '').replace(/\|/g,' ').trim(); + let fwRaw = s.split(/\s+/); let T = document.getElementById('fwScheduleTIME'); - let fwSchedD1, fwSchedX, fwXD; - if (!T) return; - // default UI state ClearTimePickerInvalid(T); - if (rawCronSchedule === 'TBD' || fwRaw.length < 5){ + if (fwRaw.length < 5) { ToggleDaysOfWeek(true, '1'); - fwSchedD1 = document.getElementById('fwSchedBoxDAYS1'); - if (fwSchedD1){ fwSchedD1.checked = true; fwSchedD1.disabled = false; } + document.getElementById('fwSchedBoxDAYS1')?.setAttribute('checked',''); T.value = '00:00'; T.disabled = false; return; } - let rawM = fwRaw[0], rawH = fwRaw[1], rawDM = fwRaw[2], rawMN = fwRaw[3], rawDW = fwRaw[4]; + let [rawM, rawH, rawDM, rawMN, rawDW] = fwRaw; // ---- Time handling: use fwScheduleTime messages only ---- let timeCheck = ValidateHHMMUsingFwScheduleTime(String(rawH) + ':' + String(rawM)); @@ -1679,7 +1726,7 @@ function InitializeFields() let fwUpdateRawCronSchedule = custom_settings.FW_New_Update_Cron_Job_Schedule || 'TBD'; FWConvertCronScheduleToWebUISettings (fwUpdateRawCronSchedule); - + MerlinAU_TimeSelectFallbackAttach('fwScheduleTIME'); SetUpEmailNotificationFields(); if (rogFWBuildType) @@ -2150,7 +2197,6 @@ function initial() UpdateScriptVersion(); showhide('Script_AutoUpdate_SchedText',true); showhide('FW_AutoUpdate_CheckSchedText',true); - MerlinAU_TimeSelectFallbackAttach('fwScheduleTIME'); // Debugging iframe behavior // var hiddenFrame = document.getElementById('hidden_frame'); From 4c72c73bc326959b80737ee3b66c39c2eda94c6b Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Tue, 2 Sep 2025 08:00:00 -0400 Subject: [PATCH 08/10] Expended Functions for Clarity Expended Functions for Clarity --- MerlinAU.asp | 592 ++++++++++++++++++++++++++++++++------------------- MerlinAU.sh | 8 - 2 files changed, 369 insertions(+), 231 deletions(-) diff --git a/MerlinAU.asp b/MerlinAU.asp index bc148099..af05aaeb 100644 --- a/MerlinAU.asp +++ b/MerlinAU.asp @@ -340,170 +340,266 @@ function ToggleDaysOfWeek (isEveryXDayChecked, numberOfDays) } } -function MerlinAU_TimeSelectFallbackAttach(id) { - var input = document.getElementById(id); - if (!input) return; +function MerlinAU_TimeSelectFallbackAttach(elementId) { + const inputElement = document.getElementById(elementId); + if (!inputElement) return; - // guard: don’t double-attach - if (input.dataset.mauTimeApplied === '1') return; + // Guard: don’t double-attach + if (inputElement.dataset.mauTimeApplied === '1') return; // Native is fine almost everywhere except Firefox. - var isFirefox = /firefox/i.test(navigator.userAgent); - var probe = document.createElement('input'); - probe.type = 'time'; - var hasNative = (probe.type === 'time'); // real time input, not text - - if (hasNative && !isFirefox) return; // keep native on Chrome/Edge/Safari/iOS/Android - - function pad(n){ return (n<10?'0':'')+n; } - function parseHHMM(v){ - v = (v||'').trim(); - var m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(v); - if (m) return {h:+m[1], m:+m[2]}; - var d = new Date(); return {h:d.getHours(), m:d.getMinutes()}; + const isFirefox = /firefox/i.test(navigator.userAgent); + const probeInput = document.createElement('input'); + probeInput.type = 'time'; + + // Real time input, not text + const hasNativeTime = (probeInput.type === 'time'); + + if (hasNativeTime && !isFirefox) { + // Keep native on Chrome/Edge/Safari/iOS/Android + return; } - var wrap = document.createElement('span'); - wrap.className = 'mau-time-fallback'; - var selH = document.createElement('select'); - var selM = document.createElement('select'); + const formatTwoDigits = (num) => String(num).padStart(2, '0'); + + function parseHHMM(value) { + const text = String(value || '').trim(); + const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(text); + if (match) { + return { hour: +match[1], minute: +match[2] }; + } + const now = new Date(); + return { hour: now.getHours(), minute: now.getMinutes() }; + } + + const wrapper = document.createElement('span'); + wrapper.className = 'mau-time-fallback'; + + const hourSelect = document.createElement('select'); + const minuteSelect = document.createElement('select'); + + // Hours: 00..23 + for (let hour = 0; hour < 24; hour += 1) { + hourSelect.add(new Option(formatTwoDigits(hour), hour)); + } - for (var h=0; h<24; h++) selH.add(new Option(pad(h), h)); - var stepSec = parseInt(input.getAttribute('step') || '60', 10); - if (isNaN(stepSec) || stepSec < 60) stepSec = 60; - var minInc = Math.max(1, Math.floor(stepSec/60)); - for (var m=0; m<60; m+=minInc) selM.add(new Option(pad(m), m)); + // Step (seconds) -> minute increment; default to 60s (1 min), clamp >= 60 + let stepSeconds = parseInt( + inputElement.getAttribute('step') || '60', + 10 + ); + if (Number.isNaN(stepSeconds) || stepSeconds < 60) stepSeconds = 60; - input.readOnly = true; - input.dataset.mauTimeApplied = '1'; + const minuteIncrement = Math.max(1, Math.floor(stepSeconds / 60)); - function emit(el, type){ - el.dispatchEvent(new Event(type, { bubbles:true })); + // Minutes: 00..59 in computed increments + for (let minute = 0; minute < 60; minute += minuteIncrement) { + minuteSelect.add(new Option(formatTwoDigits(minute), minute)); } - function apply(){ - var hh = pad(+selH.value), mm = pad(+selM.value); - input.value = hh + ':' + mm; - if (window.ClearTimePickerInvalid) window.ClearTimePickerInvalid(input); - if (window.ValidateTimePicker) window.ValidateTimePicker(input); - emit(input,'input'); emit(input,'change'); + inputElement.readOnly = true; + inputElement.dataset.mauTimeApplied = '1'; + + function dispatch(target, type) { + target.dispatchEvent(new Event(type, { bubbles: true })); } - if (input.nextSibling) input.parentNode.insertBefore(wrap, input.nextSibling); - else input.parentNode.appendChild(wrap); - wrap.appendChild(selH); - wrap.appendChild(document.createTextNode(' : ')); - wrap.appendChild(selM); + function applySelection() { + const hh = formatTwoDigits(+hourSelect.value); + const mm = formatTwoDigits(+minuteSelect.value); + inputElement.value = hh + ':' + mm; + + if (window.ClearTimePickerInvalid) { + window.ClearTimePickerInvalid(inputElement); + } + if (window.ValidateTimePicker) { + window.ValidateTimePicker(inputElement); + } - input.setAttribute('aria-hidden','true'); - input.tabIndex = -1; - selH.setAttribute('aria-label','Hour'); - selM.setAttribute('aria-label','Minute'); + dispatch(inputElement, 'input'); + dispatch(inputElement, 'change'); + } + + if (inputElement.nextSibling) { + inputElement.parentNode.insertBefore(wrapper, inputElement.nextSibling); + } else { + inputElement.parentNode.appendChild(wrapper); + } + wrapper.appendChild(hourSelect); + wrapper.appendChild(document.createTextNode(' : ')); + wrapper.appendChild(minuteSelect); - var start = parseHHMM(input.value); - selH.value = start.h; selM.value = start.m; - selH.onchange = apply; selM.onchange = apply; - var flaggedInvalid = (input.getAttribute('aria-invalid') === 'true') || - (typeof fwTimeInvalidFromConfig !== 'undefined' && fwTimeInvalidFromConfig) || - (input.classList && input.classList.contains('Invalid')); + inputElement.setAttribute('aria-hidden', 'true'); + inputElement.tabIndex = -1; + hourSelect.setAttribute('aria-label', 'Hour'); + minuteSelect.setAttribute('aria-label', 'Minute'); + + const initialTime = parseHHMM(inputElement.value); + hourSelect.value = initialTime.hour; + minuteSelect.value = initialTime.minute; + + hourSelect.onchange = applySelection; + minuteSelect.onchange = applySelection; + + const flaggedInvalid = + (inputElement.getAttribute('aria-invalid') === 'true') || + (typeof fwTimeInvalidFromConfig !== 'undefined' && + fwTimeInvalidFromConfig) || + (inputElement.classList && + inputElement.classList.contains('Invalid')); if (!flaggedInvalid) { - apply(); // normalize displayed HH:MM + // Normalize displayed HH:MM + applySelection(); } - - function syncDisabled(){ - var dis = !!input.disabled; - selH.disabled = dis; selM.disabled = dis; - wrap.style.opacity = dis ? '0.5' : '1'; + + function syncDisabled() { + const isDisabled = !!inputElement.disabled; + hourSelect.disabled = isDisabled; + minuteSelect.disabled = isDisabled; + wrapper.style.opacity = isDisabled ? '0.5' : '1'; } syncDisabled(); - var mo = new MutationObserver(syncDisabled); - mo.observe(input, { attributes:true, attributeFilter:['disabled'] }); - - function syncFromInput(){ - var v = (input.value || '').trim(); - if (!v) return; // keep current selects if input was cleared for invalid state - var t = parseHHMM(v); - selH.value = t.h; selM.value = t.m; + const mutationObserver = new MutationObserver(syncDisabled); + mutationObserver.observe(inputElement, { + attributes: true, + attributeFilter: ['disabled'] + }); + + function syncFromInput() { + const valueText = String(inputElement.value || '').trim(); + if (!valueText) return; // keep selects if input was cleared as invalid + const parsedTime = parseHHMM(valueText); + hourSelect.value = parsedTime.hour; + minuteSelect.value = parsedTime.minute; } - input.addEventListener('input', syncFromInput); - input.addEventListener('change', syncFromInput); + + inputElement.addEventListener('input', syncFromInput); + inputElement.addEventListener('change', syncFromInput); } /**---------------------------------------**/ /** Added by ExtremeFiretop [2025-Aug-24] **/ /**---------------------------------------**/ -function ParseTimeHHMM(v){ - v = (v || '').trim(); - if (!/^[0-2]\d:[0-5]\d$/.test(v)) return { ok:false }; - var h = parseInt(v.slice(0,2),10), m = parseInt(v.slice(3,5),10); - if (h < 0 || h > 23 || m < 0 || m > 59) return { ok:false }; - return { ok:true, h:h, m:m }; +function parseTimeHHMM(inputValue) { + const timeText = String(inputValue || '').trim(); + + // Shape check: "HH:MM" + if (!/^[0-2]\d:[0-5]\d$/.test(timeText)) { + return { ok: false }; + } + + const hours = parseInt(timeText.slice(0, 2), 10); + const minutes = parseInt(timeText.slice(3, 5), 10); + + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + return { ok: false }; + } + + return { ok: true, h: hours, m: minutes }; } -function ValidateHHMMUsingFwScheduleTime(hhmm){ - var v = (hhmm || '').trim(); - var parts = v.split(':'); +function ValidateHHMMUsingFwScheduleTime(timeText) { + const trimmed = String(timeText || '').trim(); + const parts = trimmed.split(':'); + // One unified message for ANY invalid time input - var commonMsg = fwScheduleTime.ErrorMsgHOUR() + '\n' + fwScheduleTime.ErrorMsgMINS(); + const commonMessage = + fwScheduleTime.ErrorMsgHOUR() + '\n' + fwScheduleTime.ErrorMsgMINS(); - if (parts.length !== 2){ - return { ok: false, msg: commonMsg }; + if (parts.length !== 2) { + return { ok: false, msg: commonMessage }; } - var H = parts[0], M = parts[1]; - var hOk = fwScheduleTime.ValidateHOUR(H); - var mOk = fwScheduleTime.ValidateMINS(M); - // Always return the same message when invalid; no hour/minute-specific messaging - return { ok: (hOk && mOk), msg: (hOk && mOk) ? '' : commonMsg }; + + const hourText = parts[0]; + const minuteText = parts[1]; + + const isHourValid = fwScheduleTime.ValidateHOUR(hourText); + const isMinuteValid = fwScheduleTime.ValidateMINS(minuteText); + const isValid = isHourValid && isMinuteValid; + + // Always return the same message when invalid; no field-specific messaging + return { ok: isValid, msg: isValid ? '' : commonMessage }; } -function MarkTimePickerInvalid(T, msg){ - if (!T) return; +function MarkTimePickerInvalid(targetElement, message) { + if (!targetElement) return; + + // Record invalid state in globals fwTimeInvalidFromConfig = true; - fwTimeInvalidMsg = msg; - - var $t = $(T); - $t.addClass('Invalid').off('.fwtime') - .on('mouseover.fwtime', function(){ return overlib(msg,0,0); }) - .on('mouseleave.fwtime input.fwtime keydown.fwtime keyup.fwtime blur.fwtime', function(){ try { nd(); } catch(e){} }); - - try { nd(); } catch(e){} - T.setAttribute('aria-invalid','true'); - T.value = ''; - - // If our fallback is attached, focus the hour select instead of the hidden input - if (T.dataset && T.dataset.mauTimeApplied === '1') { - var wrap = T.nextElementSibling; // our wrapper - var sel = wrap && wrap.querySelector('select'); - if (sel) { sel.focus(); return; } + fwTimeInvalidMsg = message; + + const $target = $(targetElement); + + $target + .addClass('Invalid') + .off('.fwtime') + .on('mouseover.fwtime', function () { + return overlib(message, 0, 0); + }) + .on( + 'mouseleave.fwtime input.fwtime keydown.fwtime keyup.fwtime blur.fwtime', + function () { + try { nd(); } catch (e) {} + } + ); + + // Ensure any existing tooltip is closed immediately + try { nd(); } catch (e) {} + + targetElement.setAttribute('aria-invalid', 'true'); + targetElement.value = ''; + + // If fallback is attached, focus the hour