8181 transition : all 0.3s ease;
8282 }
8383
84+ .time-input .invalid {
85+ border : 2px solid # dc3545 ;
86+ }
87+
8488 /* Input border colors when standards are met */
8589 tr .has-value .met-a .time-input {
8690 border : 3px solid # 28a745 ;
161165 .clear-section {
162166 text-align : center;
163167 margin-top : 2rem ;
168+ margin-bottom : 2rem ;
164169 padding-top : 2rem ;
165170 border-top : 1px solid var (--pico-muted-border-color );
166171 }
@@ -251,29 +256,33 @@ <h4>Motivational Times Lookup</h4>
251256
252257 <!-- Clear Times Section -->
253258 < section class ="clear-section ">
259+ < label style ="margin: 1rem auto; display: block; ">
260+ < input type ="checkbox " id ="fastEntryToggle " role ="switch " />
261+ Fast entry mode (numbers only)
262+ </ label >
263+ < button type ="button " id ="clearTimesBtn " class ="secondary "> Clear all times</ button >
254264 < p class ="note-text " style ="margin-bottom: 1rem; ">
255265 💡 Tip: Rotate your phone to landscape view for better table visibility.
256266 </ p >
257267 < p class ="note-text " style ="margin-bottom: 1rem; ">
258268 Times marked with ⚠️ indicate source data issues or unexpected time progressions
259269 (< a href ="https://github.com/ironprogrammer/swimcheck " target ="_blank " rel ="noopener noreferrer "> more details on GitHub</ a > ).
260270 </ p >
261- < button type ="button " id ="clearTimesBtn " class ="secondary "> Clear All Times</ button >
262271 </ section >
263272
264273 <!-- Clear Times Confirmation Modal -->
265274 < dialog id ="clearTimesModal ">
266275 < article >
267276 < header >
268277 < button aria-label ="Close " rel ="prev " id ="closeModalBtn "> </ button >
269- < p > < strong > Clear All Times? </ strong > </ p >
278+ < p > < strong > Clear all times </ strong > </ p >
270279 </ header >
271280 < p >
272281 Are you sure you want to clear all entered times? This cannot be undone.
273282 </ p >
274283 < footer >
275284 < button type ="button " class ="secondary " id ="cancelClearBtn "> Cancel</ button >
276- < button type ="button " id ="confirmClearBtn "> Clear All Times </ button >
285+ < button type ="button " id ="confirmClearBtn "> Clear all times </ button >
277286 </ footer >
278287 </ article >
279288 </ dialog >
@@ -301,13 +310,21 @@ <h4>Motivational Times Lookup</h4>
301310 gender : null ,
302311 courseType : 'SCY' ,
303312 times : { } ,
304- dataVersion : CURRENT_DATA_VERSION
313+ dataVersion : CURRENT_DATA_VERSION ,
314+ fastEntryMode : false
305315 } ;
306316
307317 // Load JSON data
308318 async function loadData ( ) {
309319 try {
310- const response = await fetch ( 'swim_time_standards.json' ) ;
320+ // Try local file first
321+ let response = await fetch ( 'swim_time_standards.json' ) ;
322+
323+ // If local file doesn't exist, fall back to swimcheck.org
324+ if ( ! response . ok ) {
325+ response = await fetch ( 'https://swimcheck.org/swim_time_standards.json' ) ;
326+ }
327+
311328 swimData = await response . json ( ) ;
312329 document . getElementById ( 'seasonInfo' ) . textContent = swimData . title ;
313330 initializeApp ( ) ;
@@ -475,6 +492,23 @@ <h4>Motivational Times Lookup</h4>
475492 }
476493 }
477494
495+ // Fast entry mode toggle
496+ const fastEntryToggle = document . getElementById ( 'fastEntryToggle' ) ;
497+ fastEntryToggle . checked = appState . fastEntryMode ;
498+ fastEntryToggle . addEventListener ( 'change' , ( e ) => {
499+ appState . fastEntryMode = e . target . checked ;
500+ saveStateToStorage ( ) ;
501+
502+ // Update inputmode on all existing time inputs
503+ document . querySelectorAll ( '.time-input' ) . forEach ( input => {
504+ if ( appState . fastEntryMode ) {
505+ input . setAttribute ( 'inputmode' , 'numeric' ) ;
506+ } else {
507+ input . removeAttribute ( 'inputmode' ) ;
508+ }
509+ } ) ;
510+ } ) ;
511+
478512 // Clear times button - show modal
479513 document . getElementById ( 'clearTimesBtn' ) . addEventListener ( 'click' , ( ) => {
480514 document . getElementById ( 'clearTimesModal' ) . showModal ( ) ;
@@ -572,13 +606,16 @@ <h4>Motivational Times Lookup</h4>
572606 const bPlusClass = qualification === 'b-plus' ? 'standard-cell met met-b-plus' : 'standard-cell' ;
573607 const bClass = qualification === 'b' ? 'standard-cell met met-b' : 'standard-cell' ;
574608
609+ const inputModeAttr = appState . fastEntryMode ? 'inputmode="numeric"' : '' ;
610+
575611 tableHTML += `
576612 <tr class="${ rowClass } ">
577613 <td>${ event . name } </td>
578614 <td>
579615 <input
580616 type="text"
581617 class="time-input"
618+ ${ inputModeAttr }
582619 placeholder="mm:ss.ms"
583620 data-event-key="${ eventKey } "
584621 value="${ userTime } "
@@ -616,7 +653,27 @@ <h4>Motivational Times Lookup</h4>
616653 inputs . forEach ( input => {
617654 let timeout ;
618655
656+ // Auto-select text on focus for easier replacement
657+ input . addEventListener ( 'focus' , ( e ) => {
658+ e . target . select ( ) ;
659+
660+ // Prevent mouseup from repositioning cursor after select
661+ const preventMouseUp = ( event ) => {
662+ event . preventDefault ( ) ;
663+ e . target . removeEventListener ( 'mouseup' , preventMouseUp ) ;
664+ } ;
665+ e . target . addEventListener ( 'mouseup' , preventMouseUp , { once : true } ) ;
666+ } ) ;
667+
619668 input . addEventListener ( 'input' , ( e ) => {
669+ // Real-time validation feedback
670+ const value = e . target . value . trim ( ) ;
671+ if ( value && ! isValidTimeInput ( value ) ) {
672+ e . target . classList . add ( 'invalid' ) ;
673+ } else {
674+ e . target . classList . remove ( 'invalid' ) ;
675+ }
676+
620677 clearTimeout ( timeout ) ;
621678 timeout = setTimeout ( ( ) => {
622679 handleTimeInput ( e . target ) ;
@@ -630,13 +687,70 @@ <h4>Motivational Times Lookup</h4>
630687 } ) ;
631688 }
632689
690+ // Validate time input contains only valid characters
691+ function isValidTimeInput ( value ) {
692+ if ( ! value ) return true ;
693+ return / ^ [ 0 - 9 : . ] + $ / . test ( value ) ;
694+ }
695+
696+ // Parse time input: strip non-digits, parse right-to-left to mm:ss.xx format
697+ function parseTimeInput ( value ) {
698+ if ( ! value ) return '' ;
699+
700+ // Strip all non-digits and leading zeros
701+ let digits = value . replace ( / \D / g, '' ) ;
702+ digits = digits . replace ( / ^ 0 + / , '' ) || '0' ; // Remove leading zeros, keep at least one digit
703+ if ( digits . length === 0 ) return '' ;
704+
705+ const len = digits . length ;
706+
707+ if ( len === 1 ) {
708+ // Single digit: "5" → "00.05"
709+ return `00.0${ digits } ` ;
710+ } else if ( len === 2 ) {
711+ // Two digits: "34" → "00.34"
712+ return `00.${ digits } ` ;
713+ } else if ( len === 3 ) {
714+ // Three digits: "123" → "01.23"
715+ return `0${ digits [ 0 ] } .${ digits . slice ( 1 ) } ` ;
716+ } else if ( len === 4 ) {
717+ // Four digits: "1234" → "12.34"
718+ return `${ digits . slice ( 0 , 2 ) } .${ digits . slice ( 2 ) } ` ;
719+ } else {
720+ // Five or more digits: "12345" → "1:23.45", "123456" → "12:34.56"
721+ const hundredths = digits . slice ( - 2 ) ;
722+ const seconds = digits . slice ( - 4 , - 2 ) ;
723+ const minutes = digits . slice ( 0 , - 4 ) ;
724+ return `${ minutes } :${ seconds } .${ hundredths } ` ;
725+ }
726+ }
727+
633728 // Handle time input
634729 function handleTimeInput ( input ) {
635730 const eventKey = input . dataset . eventKey ;
636731 const value = input . value . trim ( ) ;
637732
638- if ( value ) {
639- appState . times [ eventKey ] = value ;
733+ // Check for invalid characters
734+ if ( value && ! isValidTimeInput ( value ) ) {
735+ input . classList . add ( 'invalid' ) ;
736+ delete appState . times [ eventKey ] ;
737+ saveStateToStorage ( ) ;
738+ updateStandardHighlighting ( input ) ;
739+ return ;
740+ }
741+
742+ // Remove invalid class if present
743+ input . classList . remove ( 'invalid' ) ;
744+
745+ // Parse and format the time
746+ const parsedTime = parseTimeInput ( value ) ;
747+
748+ // Enforce max length of 9 characters (e.g., "190:38.98")
749+ const finalTime = parsedTime . substring ( 0 , 9 ) ;
750+
751+ if ( finalTime ) {
752+ appState . times [ eventKey ] = finalTime ;
753+ input . value = finalTime ;
640754 } else {
641755 delete appState . times [ eventKey ] ;
642756 }
@@ -672,6 +786,11 @@ <h4>Motivational Times Lookup</h4>
672786 return ;
673787 }
674788
789+ // Skip comparison if input is invalid
790+ if ( input . classList . contains ( 'invalid' ) ) {
791+ return ;
792+ }
793+
675794 // Find the event's times
676795 const [ gender , course , eventName ] = eventKey . split ( '_' ) ;
677796 const ageGroupData = swimData . ageGroups . find ( g => g . age === appState . ageGroup ) ;
0 commit comments