2121 * @property {CronSample? } sample
2222 * @property {InitialValue? } value
2323 * @property {('x_hours'|'x_days'|'certain_days'|'custom'))[]? } modes
24+ * @property {boolean? } hideExpression
2425 * @property {((expr: string) => void)? } onChange
2526 */
2627import { getRandomId , getValue , loadStylesheet } from '../utils.js' ;
@@ -92,6 +93,7 @@ const CrontabInput = (/** @type Options */ props) => {
9293 onClose : ( ) => opened . val = false ,
9394 sample : props . sample ,
9495 modes : props . modes ,
96+ hideExpression : props . hideExpression ,
9597 } ,
9698 expression ,
9799 ) ,
@@ -110,11 +112,13 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
110112 const xHoursState = {
111113 hours : van . state ( 1 ) ,
112114 minute : van . state ( 0 ) ,
115+ startHour : van . state ( 0 ) ,
113116 } ;
114117 const xDaysState = {
115118 days : van . state ( 1 ) ,
116119 hour : van . state ( 1 ) ,
117120 minute : van . state ( 0 ) ,
121+ startDay : van . state ( 1 ) ,
118122 } ;
119123 const certainDaysState = {
120124 sunday : van . state ( false ) ,
@@ -135,12 +139,30 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
135139 if ( mode . val === 'x_hours' ) {
136140 const hours = xHoursState . hours . val ;
137141 const minute = xHoursState . minute . val ;
138- options . onChange ( `${ minute ?? 0 } ${ ( hours && hours !== 1 ) ? '*/' + hours : '*' } * * *` ) ;
142+ const startHour = xHoursState . startHour . val ;
143+ let hourField ;
144+ if ( ! hours || hours <= 1 ) {
145+ hourField = '*' ;
146+ } else if ( startHour > 0 ) {
147+ hourField = generateSteppedValues ( startHour , hours , 23 ) ;
148+ } else {
149+ hourField = '*/' + hours ;
150+ }
151+ options . onChange ( `${ minute ?? 0 } ${ hourField } * * *` ) ;
139152 } else if ( mode . val === 'x_days' ) {
140153 const days = xDaysState . days . val ;
141154 const hour = xDaysState . hour . val ;
142155 const minute = xDaysState . minute . val ;
143- options . onChange ( `${ minute ?? 0 } ${ hour ?? 0 } ${ ( days && days !== 1 ) ? '*/' + days : '*' } * *` ) ;
156+ const startDay = xDaysState . startDay . val ;
157+ let dayField ;
158+ if ( ! days || days <= 1 ) {
159+ dayField = '*' ;
160+ } else if ( startDay > 1 ) {
161+ dayField = generateSteppedValues ( startDay , days , 31 ) ;
162+ } else {
163+ dayField = '*/' + days ;
164+ }
165+ options . onChange ( `${ minute ?? 0 } ${ hour ?? 0 } ${ dayField } * *` ) ;
144166 } else if ( mode . val === 'certain_days' ) {
145167 const days = [ ] ;
146168 const dayMap = [
@@ -225,16 +247,19 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
225247 span ( { } , 'Every' ) ,
226248 ( ) => Select ( {
227249 label : "" ,
228- options : Array . from ( { length : 24 } , ( _ , i ) => i ) . map ( i => ( { label : i . toString ( ) , value : i } ) ) ,
250+ options : Array . from ( { length : 24 } , ( _ , i ) => i + 1 ) . map ( i => ( { label : i . toString ( ) , value : i } ) ) ,
229251 triggerStyle : 'inline' ,
230252 portalClass : 'tg-crontab--select-portal' ,
231253 value : xHoursState . hours ,
232- onChange : ( value ) => xHoursState . hours . val = value ,
254+ onChange : ( value ) => {
255+ xHoursState . hours . val = value ;
256+ if ( value <= 1 ) xHoursState . startHour . val = 0 ;
257+ } ,
233258 } ) ,
234259 span ( { } , 'hours' ) ,
235260 ) ,
236261 div (
237- { class : ' flex-row fx-gap-2' } ,
262+ { class : ( ) => ` flex-row fx-gap-2 ${ xHoursState . hours . val > 1 ? 'mb-2' : '' } ` } ,
238263 span ( { } , 'on' ) ,
239264 span ( { } , 'minute' ) ,
240265 ( ) => Select ( {
@@ -246,6 +271,18 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
246271 onChange : ( value ) => xHoursState . minute . val = value ,
247272 } ) ,
248273 ) ,
274+ div (
275+ { class : ( ) => `flex-row fx-gap-2 ${ xHoursState . hours . val > 1 ? '' : 'hidden' } ` } ,
276+ span ( { } , 'starting at hour' ) ,
277+ ( ) => Select ( {
278+ label : "" ,
279+ options : Array . from ( { length : 24 } , ( _ , i ) => i ) . map ( i => ( { label : i . toString ( ) , value : i } ) ) ,
280+ triggerStyle : 'inline' ,
281+ portalClass : 'tg-crontab--select-portal' ,
282+ value : xHoursState . startHour ,
283+ onChange : ( value ) => xHoursState . startHour . val = value ,
284+ } ) ,
285+ ) ,
249286 ) ,
250287 div (
251288 { class : ( ) => `${ mode . val === 'x_days' ? '' : 'hidden' } ` } ,
@@ -258,12 +295,15 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
258295 triggerStyle : 'inline' ,
259296 portalClass : 'tg-crontab--select-portal' ,
260297 value : xDaysState . days ,
261- onChange : ( value ) => xDaysState . days . val = value ,
298+ onChange : ( value ) => {
299+ xDaysState . days . val = value ;
300+ if ( value <= 1 ) xDaysState . startDay . val = 1 ;
301+ } ,
262302 } ) ,
263303 span ( { } , 'days' ) ,
264304 ) ,
265305 div (
266- { class : ' flex-row fx-gap-2' } ,
306+ { class : ( ) => ` flex-row fx-gap-2 ${ xDaysState . days . val > 1 ? 'mb-2' : '' } ` } ,
267307 span ( { } , 'at' ) ,
268308 ( ) => Select ( {
269309 label : "" ,
@@ -282,6 +322,18 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
282322 onChange : ( value ) => xDaysState . minute . val = value ,
283323 } ) ,
284324 ) ,
325+ div (
326+ { class : ( ) => `flex-row fx-gap-2 ${ xDaysState . days . val > 1 ? '' : 'hidden' } ` } ,
327+ span ( { } , 'starting on day' ) ,
328+ ( ) => Select ( {
329+ label : "" ,
330+ options : Array . from ( { length : 31 } , ( _ , i ) => i + 1 ) . map ( i => ( { label : i . toString ( ) , value : i } ) ) ,
331+ triggerStyle : 'inline' ,
332+ portalClass : 'tg-crontab--select-portal' ,
333+ value : xDaysState . startDay ,
334+ onChange : ( value ) => xDaysState . startDay . val = value ,
335+ } ) ,
336+ ) ,
285337 ) ,
286338 div (
287339 { class : ( ) => `${ mode . val === 'certain_days' ? '' : 'hidden' } ` } ,
@@ -370,7 +422,7 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
370422 div (
371423 { class : 'flex-column fx-gap-1 mt-3 text-secondary' } ,
372424 ( ) => span (
373- { class : mode . val === 'custom' ? 'hidden' : '' } ,
425+ { class : mode . val === 'custom' || getValue ( options . hideExpression ) ? 'hidden' : '' } ,
374426 `Cron Expression: ${ expr . val ?? '' } ` ,
375427 ) ,
376428 ( ) => div (
@@ -409,6 +461,25 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
409461 ) ;
410462} ;
411463
464+ function generateSteppedValues ( start , step , max ) {
465+ const values = [ ] ;
466+ for ( let i = start ; i <= max ; i += step ) {
467+ values . push ( i ) ;
468+ }
469+ return values . join ( ',' ) ;
470+ }
471+
472+ function parseSteppedList ( field ) {
473+ const values = field . split ( ',' ) . map ( Number ) ;
474+ if ( values . length < 2 || values . some ( isNaN ) ) return null ;
475+ const step = values [ 1 ] - values [ 0 ] ;
476+ if ( step <= 0 ) return null ;
477+ for ( let i = 2 ; i < values . length ; i ++ ) {
478+ if ( values [ i ] - values [ i - 1 ] !== step ) return null ;
479+ }
480+ return { start : values [ 0 ] , step } ;
481+ }
482+
412483/**
413484 * Populates the state variables for the initial mode based on the cron expression
414485 * @param {string } expr
@@ -420,21 +491,35 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
420491function populateInitialModeState ( expr , mode , xHoursState , xDaysState , certainDaysState ) {
421492 const parts = ( expr || '' ) . trim ( ) . split ( / \s + / ) ;
422493 if ( mode === 'x_hours' && parts . length === 5 ) {
423- // e.g. "M */H * * *" or "M * * * *"
424494 xHoursState . minute . val = Number ( parts [ 0 ] ) || 0 ;
425495 if ( parts [ 1 ] . startsWith ( '*/' ) ) {
426496 xHoursState . hours . val = Number ( parts [ 1 ] . slice ( 2 ) ) || 1 ;
497+ xHoursState . startHour . val = 0 ;
498+ } else if ( parts [ 1 ] . includes ( ',' ) ) {
499+ const parsed = parseSteppedList ( parts [ 1 ] ) ;
500+ if ( parsed ) {
501+ xHoursState . hours . val = parsed . step ;
502+ xHoursState . startHour . val = parsed . start ;
503+ }
427504 } else {
428505 xHoursState . hours . val = 1 ;
506+ xHoursState . startHour . val = 0 ;
429507 }
430508 } else if ( mode === 'x_days' && parts . length === 5 ) {
431- // e.g. "M H */D * *" or "M H * * *"
432509 xDaysState . minute . val = Number ( parts [ 0 ] ) || 0 ;
433510 xDaysState . hour . val = Number ( parts [ 1 ] ) || 0 ;
434511 if ( parts [ 2 ] . startsWith ( '*/' ) ) {
435512 xDaysState . days . val = Number ( parts [ 2 ] . slice ( 2 ) ) || 1 ;
513+ xDaysState . startDay . val = 1 ;
514+ } else if ( parts [ 2 ] . includes ( ',' ) ) {
515+ const parsed = parseSteppedList ( parts [ 2 ] ) ;
516+ if ( parsed ) {
517+ xDaysState . days . val = parsed . step ;
518+ xDaysState . startDay . val = parsed . start ;
519+ }
436520 } else {
437521 xDaysState . days . val = 1 ;
522+ xDaysState . startDay . val = 1 ;
438523 }
439524 } else if ( mode === 'certain_days' && parts . length === 5 ) {
440525 // e.g. "M H * * DAY[,DAY...]"
@@ -465,14 +550,22 @@ function populateInitialModeState(expr, mode, xHoursState, xDaysState, certainDa
465550function determineMode ( expression ) {
466551 // Normalize whitespace
467552 const expr = ( expression || '' ) . trim ( ) . replace ( / \s + / g, ' ' ) ;
468- // x_hours: "M */H * * *" or "M * * * *"
553+ // x_hours: "M */H * * *" or "M * * * *" or "M H1,H2,... * * *"
469554 if ( / ^ \d { 1 , 2 } \* \/ \d + \* \* \* $ / . test ( expr ) || / ^ \d { 1 , 2 } \* \* \* \* $ / . test ( expr ) ) {
470555 return 'x_hours' ;
471556 }
472- // x_days: "M H */D * *" or "M H * * *"
557+ if ( / ^ \d { 1 , 2 } \d + ( , \d + ) + \* \* \* $ / . test ( expr ) ) {
558+ const hourField = expr . split ( ' ' ) [ 1 ] ;
559+ if ( parseSteppedList ( hourField ) ) return 'x_hours' ;
560+ }
561+ // x_days: "M H */D * *" or "M H * * *" or "M H D1,D2,... * *"
473562 if ( / ^ \d { 1 , 2 } \d { 1 , 2 } \* \/ \d + \* \* $ / . test ( expr ) || / ^ \d { 1 , 2 } \d { 1 , 2 } \* \* \* $ / . test ( expr ) ) {
474563 return 'x_days' ;
475564 }
565+ if ( / ^ \d { 1 , 2 } \d { 1 , 2 } \d + ( , \d + ) + \* \* $ / . test ( expr ) ) {
566+ const dayField = expr . split ( ' ' ) [ 2 ] ;
567+ if ( parseSteppedList ( dayField ) ) return 'x_days' ;
568+ }
476569 // certain_days: "M H * * DAY[,DAY...]" (DAY = SUN,MON,...)
477570 if ( / ^ \d { 1 , 2 } \d { 1 , 2 } \* \* ( ( S U N | M O N | T U E | W E D | T H U | F R I | S A T ) ( - ( S U N | M O N | T U E | W E D | T H U | F R I | S A T ) ) ? ( , ) ? ) + $ / . test ( expr ) ) {
478571 return 'certain_days' ;
@@ -533,4 +626,4 @@ stylesheet.replace(`
533626}
534627` ) ;
535628
536- export { CrontabInput } ;
629+ export { CrontabInput , parseSteppedList } ;
0 commit comments