diff --git a/MerlinAU.asp b/MerlinAU.asp index 89c57a3e..03a6064e 100644 --- a/MerlinAU.asp +++ b/MerlinAU.asp @@ -39,6 +39,9 @@ var shared_custom_settings = {}; var ajax_custom_settings = {}; 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"]; @@ -337,38 +340,267 @@ 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; } +function MerlinAU_TimeSelectFallbackAttach(elementId) { + const inputElement = document.getElementById(elementId); + if (!inputElement) return; + + // Guard: don’t double-attach + if (inputElement.dataset.mauTimeApplied === '1') return; + + // Native is fine almost everywhere except Firefox. + 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; + } + + 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'; + wrapper.style.marginLeft = '10px'; // add spacing between input and 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)); + } + + // 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; + + const minuteIncrement = Math.max(1, Math.floor(stepSeconds / 60)); + + // Minutes: 00..59 in computed increments + for (let minute = 0; minute < 60; minute += minuteIncrement) { + minuteSelect.add(new Option(formatTwoDigits(minute), minute)); + } + + inputElement.readOnly = true; + inputElement.dataset.mauTimeApplied = '1'; + + function dispatch(target, type) { + target.dispatchEvent(new Event(type, { bubbles: true })); + } + + 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); + } + + 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); + + 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) { + // Normalize displayed HH:MM + applySelection(); + } + + function syncDisabled() { + const isDisabled = !!inputElement.disabled; + hourSelect.disabled = isDisabled; + minuteSelect.disabled = isDisabled; + wrapper.style.opacity = isDisabled ? '0.5' : '1'; + } + syncDisabled(); + + 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; + } + + inputElement.addEventListener('input', syncFromInput); + inputElement.addEventListener('change', syncFromInput); } -/**-------------------------------------**/ -/** 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; } +/**---------------------------------------**/ +/** Added by ExtremeFiretop [2025-Aug-24] **/ +/**---------------------------------------**/ +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(timeText) { + const trimmed = String(timeText || '').trim(); + const parts = trimmed.split(':'); + + // One unified message for ANY invalid time input + const commonMessage = + fwScheduleTime.ErrorMsgHOUR() + '\n' + fwScheduleTime.ErrorMsgMINS(); + + if (parts.length !== 2) { + return { ok: false, msg: commonMessage }; + } + + 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(targetElement, message) { + if (!targetElement) return; + + // Record invalid state in globals + fwTimeInvalidFromConfig = true; + 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 Sat
-
- Hour: - -
-
- Minutes: - + +
+ Time: +
diff --git a/MerlinAU.sh b/MerlinAU.sh index 736352d1..4b7ccb00 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -10,7 +10,7 @@ set -u ## Set version for each Production Release ## readonly SCRIPT_VERSION=1.5.4 -readonly SCRIPT_VERSTAG="25090123" +readonly SCRIPT_VERSTAG="25090208" 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