@@ -19,6 +19,7 @@ import {
19
19
import { hourRange , toMoment , getHourSpan , DEFAULT_TIME_FORMAT } from '../../../util/date' ;
20
20
import { useIsSmallScreen } from '../../../util/hooks' ;
21
21
import TimezonePicker from '../../common/TimezonePicker' ;
22
+ import CandidatePlaceholder from './CandidatePlaceholder' ;
22
23
import CandidateSlot from './CandidateSlot' ;
23
24
import DurationPicker from './DurationPicker' ;
24
25
import TimelineHeader from './TimelineHeader' ;
@@ -171,6 +172,17 @@ function TimelineInput({minHour, maxHour}) {
171
172
const latestStartTime = useSelector ( getNewTimeslotStartTime ) ;
172
173
const [ timeslotTime , setTimeslotTime ] = useState ( latestStartTime ) ;
173
174
const [ newTimeslotPopupOpen , setTimeslotPopupOpen ] = useState ( false ) ;
175
+ // Indicates the position of the mouse in the timeline
176
+ const [ candidatePlaceholder , setCandidatePlaceholder ] = useState ( {
177
+ visible : false ,
178
+ time : '' ,
179
+ x : 0 ,
180
+ y : 0 ,
181
+ } ) ;
182
+ // We don't want to show the tooltip when the mouse is hovering over a slot
183
+ const [ isHoveringSlot , setIsHoveringSlot ] = useState ( false ) ;
184
+ // const baseWidth = calculateWidth("00:00", duration, minHour, maxHour);
185
+ const placeHolderSlot = getCandidateSlotProps ( '00:00' , duration , minHour , maxHour ) ;
174
186
175
187
useEffect ( ( ) => {
176
188
setTimeslotTime ( latestStartTime ) ;
@@ -196,92 +208,228 @@ function TimelineInput({minHour, maxHour}) {
196
208
dispatch ( addTimeslot ( date , time ) ) ;
197
209
} ;
198
210
199
- const handleRemoveSlot = time => {
211
+ const handleRemoveSlot = ( event , time ) => {
200
212
dispatch ( removeTimeslot ( date , time ) ) ;
213
+ setIsHoveringSlot ( false ) ;
201
214
} ;
202
215
203
216
const handleUpdateSlot = ( oldTime , newTime ) => {
204
217
dispatch ( removeTimeslot ( date , oldTime ) ) ;
205
218
dispatch ( addTimeslot ( date , newTime ) ) ;
206
219
} ;
207
220
221
+ const handleMouseDown = e => {
222
+ const parentRect = e . target . getBoundingClientRect ( ) ;
223
+ const totalMinutes = ( maxHour - minHour ) * 60 ;
224
+
225
+ // Get the parent rect start position
226
+ const parentRectStart = parentRect . left ;
227
+ // Get the parent rect end position
228
+ const parentRectEnd = parentRect . right ;
229
+
230
+ const clickPositionRelative = ( e . clientX - parentRectStart ) / ( parentRectEnd - parentRectStart ) ;
231
+
232
+ let clickTimeRelative = clickPositionRelative * totalMinutes ;
233
+
234
+ // Round clickTimeRelative to the nearest 15-minute interval
235
+ clickTimeRelative = Math . round ( clickTimeRelative / 15 ) * 15 ;
236
+
237
+ // Convert clickTimeRelative to a time format (HH:mm)
238
+ const clickTimeRelativeTime = moment ( )
239
+ . startOf ( 'day' )
240
+ . add ( clickTimeRelative , 'minutes' )
241
+ . format ( 'HH:mm' ) ;
242
+
243
+ const canBeAdded = clickTimeRelativeTime && ! candidates . includes ( clickTimeRelativeTime ) ;
244
+ if ( canBeAdded ) {
245
+ handleAddSlot ( clickTimeRelativeTime ) ;
246
+ }
247
+ } ;
248
+
249
+ /**
250
+ * Tracks the mouse movement in the timeline and updates the candidatePlaceholder state
251
+ * @param {Event } e
252
+ * @returns
253
+ */
254
+ const handleTimelineMouseMove = e => {
255
+ if ( isHoveringSlot ) {
256
+ setCandidatePlaceholder ( { visible : false } ) ;
257
+ return ;
258
+ }
259
+ const parentRect = e . target . getBoundingClientRect ( ) ;
260
+ const relativeMouseXPosition = e . clientX - parentRect . left ;
261
+ const relativeMouseYPosition = parentRect . top - e . clientY ;
262
+
263
+ const totalMinutes = ( maxHour - minHour ) * 60 ; // Total minutes in the timeline
264
+ let timeInMinutes = ( relativeMouseXPosition / parentRect . width ) * totalMinutes ;
265
+ // Round timeInMinutes to the nearest 15-minute interval
266
+ timeInMinutes = Math . round ( timeInMinutes / 15 ) * 15 ;
267
+ const slotWidth = ( placeHolderSlot . width / parentRect . width ) * 100 ;
268
+
269
+ const hours = Math . floor ( timeInMinutes / 60 ) + minHour ;
270
+ const minutes = Math . floor ( timeInMinutes % 60 ) ;
271
+ const time = `${ String ( hours ) . padStart ( 2 , '0' ) } :${ String ( minutes ) . padStart ( 2 , '0' ) } ` ;
272
+
273
+ const tempPlaceholder = {
274
+ visible : true ,
275
+ time,
276
+ x : relativeMouseXPosition ,
277
+ y : relativeMouseYPosition ,
278
+ clientX : e . clientX ,
279
+ clientY : parentRect . top + parentRect . height ,
280
+ height : parentRect . height ,
281
+ parentHeight : parentRect . height - 10 ,
282
+ width : slotWidth ,
283
+ } ;
284
+
285
+ if ( hours >= 0 && minutes >= 0 ) {
286
+ setCandidatePlaceholder ( tempPlaceholder ) ;
287
+ }
288
+ } ;
289
+
290
+ const handleTimelineMouseLeave = ( ) => {
291
+ setCandidatePlaceholder ( { visible : false } ) ;
292
+ } ;
293
+
208
294
const groupedCandidates = splitOverlappingCandidates ( candidates , duration ) ;
209
295
210
296
return editing ? (
211
- < div className = { `${ styles [ 'timeline-input' ] } ${ styles [ 'edit' ] } ` } >
212
- < div className = { styles [ 'timeline-candidates' ] } >
213
- { groupedCandidates . map ( ( rowCandidates , i ) => (
214
- < div className = { styles [ 'candidates-group' ] } key = { i } >
215
- { rowCandidates . map ( time => {
216
- const slotProps = getCandidateSlotProps ( time , duration , minHour , maxHour ) ;
217
- const participants = availability ?. find ( a => a . startDt === `${ date } T${ time } ` ) ;
218
- return (
219
- < CandidateSlot
220
- { ...slotProps }
221
- key = { time }
222
- isValidTime = { time => ! candidates . includes ( time ) }
223
- onDelete = { ( ) => handleRemoveSlot ( time ) }
224
- onChangeSlotTime = { newStartTime => handleUpdateSlot ( time , newStartTime ) }
225
- text = {
226
- participants &&
227
- plural ( participants . availableCount , {
228
- 0 : 'No participants registered' ,
229
- one : '# participant registered' ,
230
- other : '# participants registered' ,
231
- } )
232
- }
233
- />
234
- ) ;
235
- } ) }
297
+ < Popup
298
+ content = { candidatePlaceholder . time }
299
+ open = { candidatePlaceholder . visible }
300
+ popperModifiers = { [
301
+ {
302
+ name : 'offset' ,
303
+ enabled : true ,
304
+ options : {
305
+ offset : [ candidatePlaceholder . x , 0 ] ,
306
+ } ,
307
+ } ,
308
+ ] }
309
+ trigger = {
310
+ < div
311
+ className = { `${ styles [ 'timeline-input' ] } ${ styles [ 'edit' ] } ` }
312
+ onClick = { event => handleMouseDown ( event ) }
313
+ onMouseMove = { handleTimelineMouseMove }
314
+ onMouseLeave = { handleTimelineMouseLeave }
315
+ >
316
+ < div className = { styles [ 'timeline-candidates' ] } >
317
+ { groupedCandidates . map ( ( rowCandidates , i ) => (
318
+ < div
319
+ className = { styles [ 'candidates-group' ] }
320
+ key = { i }
321
+ onMouseEnter = { ( ) => {
322
+ // Prevent the candidate placeholder from showing when hovering over a slot
323
+ setIsHoveringSlot ( true ) ;
324
+ } }
325
+ onMouseLeave = { ( ) => {
326
+ setIsHoveringSlot ( false ) ;
327
+ } }
328
+ >
329
+ { rowCandidates . map ( time => {
330
+ const slotProps = getCandidateSlotProps ( time , duration , minHour , maxHour ) ;
331
+ const participants = availability ?. find ( a => a . startDt === `${ date } T${ time } ` ) ;
332
+ return (
333
+ < CandidateSlot
334
+ { ...slotProps }
335
+ key = { time }
336
+ isValidTime = { time => ! candidates . includes ( time ) }
337
+ onDelete = { event => {
338
+ // Prevent the event from bubbling up to the parent div
339
+ event . stopPropagation ( ) ;
340
+ handleRemoveSlot ( event , time ) ;
341
+ } }
342
+ onChangeSlotTime = { newStartTime => handleUpdateSlot ( time , newStartTime ) }
343
+ text = {
344
+ participants &&
345
+ plural ( participants . availableCount , {
346
+ 0 : 'No participants registered' ,
347
+ one : '# participant registered' ,
348
+ other : '# participants registered' ,
349
+ } )
350
+ }
351
+ />
352
+ ) ;
353
+ } ) }
354
+ </ div >
355
+ ) ) }
356
+ { candidatePlaceholder . visible && (
357
+ < CandidatePlaceholder
358
+ xPosition = { candidatePlaceholder . clientX }
359
+ yPosition = { candidatePlaceholder . clientY }
360
+ height = { candidatePlaceholder . parentHeight }
361
+ widthPercent = { candidatePlaceholder . width }
362
+ />
363
+ ) }
236
364
</ div >
237
- ) ) }
238
- </ div >
239
- < Popup
240
- trigger = {
241
- < Icon
242
- className = { `${ styles [ 'clickable' ] } ${ styles [ 'add-btn' ] } ` }
243
- name = "plus circle"
244
- size = "large"
245
- />
246
- }
247
- on = "click"
248
- position = "bottom center"
249
- onOpen = { ( ) => setTimeslotPopupOpen ( true ) }
250
- onClose = { handlePopupClose }
251
- open = { newTimeslotPopupOpen }
252
- onKeyDown = { evt => {
253
- const canBeAdded = timeslotTime && ! candidates . includes ( timeslotTime ) ;
254
- if ( evt . key === 'Enter' && canBeAdded ) {
255
- handleAddSlot ( timeslotTime ) ;
256
- handlePopupClose ( ) ;
257
- }
258
- } }
259
- className = { styles [ 'timepicker-popup' ] }
260
- content = {
261
- < >
262
- < TimePicker
263
- showSecond = { false }
264
- value = { toMoment ( timeslotTime , DEFAULT_TIME_FORMAT ) }
265
- format = { DEFAULT_TIME_FORMAT }
266
- onChange = { time => setTimeslotTime ( time ? time . format ( DEFAULT_TIME_FORMAT ) : null ) }
267
- allowEmpty = { false }
268
- // keep the picker in the DOM tree of the surrounding element
269
- getPopupContainer = { node => node }
270
- />
271
- < Button
272
- icon
273
- onClick = { ( ) => {
274
- handleAddSlot ( timeslotTime ) ;
275
- handlePopupClose ( ) ;
365
+ < div onMouseMove = { e => e . stopPropagation ( ) } className = { styles [ 'add-btn-wrapper' ] } >
366
+ < Popup
367
+ trigger = {
368
+ < Icon
369
+ className = { `${ styles [ 'clickable' ] } ${ styles [ 'add-btn' ] } ` }
370
+ name = "plus circle"
371
+ size = "large"
372
+ onMouseMove = { e => e . stopPropagation ( ) }
373
+ />
374
+ }
375
+ on = "click"
376
+ onMouseMove = { e => {
377
+ e . stopPropagation ( ) ;
276
378
} }
277
- disabled = { ! timeslotTime || candidates . includes ( timeslotTime ) }
278
- >
279
- < Icon name = "check" />
280
- </ Button >
281
- </ >
282
- }
283
- />
284
- </ div >
379
+ position = "bottom center"
380
+ onOpen = { evt => {
381
+ // Prevent the event from bubbling up to the parent div
382
+ evt . stopPropagation ( ) ;
383
+ setTimeslotPopupOpen ( true ) ;
384
+ } }
385
+ onClose = { handlePopupClose }
386
+ open = { newTimeslotPopupOpen }
387
+ onKeyDown = { evt => {
388
+ const canBeAdded = timeslotTime && ! candidates . includes ( timeslotTime ) ;
389
+ if ( evt . key === 'Enter' && canBeAdded ) {
390
+ handleAddSlot ( timeslotTime ) ;
391
+ handlePopupClose ( ) ;
392
+ }
393
+ } }
394
+ className = { styles [ 'timepicker-popup' ] }
395
+ content = {
396
+ < div
397
+ // We need a div to attach events
398
+ onClick = { e => e . stopPropagation ( ) }
399
+ onMouseMove = { e => {
400
+ e . stopPropagation ( ) ;
401
+ } }
402
+ >
403
+ < TimePicker
404
+ showSecond = { false }
405
+ value = { toMoment ( timeslotTime , DEFAULT_TIME_FORMAT ) }
406
+ format = { DEFAULT_TIME_FORMAT }
407
+ onChange = { time =>
408
+ setTimeslotTime ( time ? time . format ( DEFAULT_TIME_FORMAT ) : null )
409
+ }
410
+ onMouseMove = { e => e . stopPropagation ( ) }
411
+ allowEmpty = { false }
412
+ // keep the picker in the DOM tree of the surrounding element
413
+ getPopupContainer = { node => node }
414
+ />
415
+ < Button
416
+ icon
417
+ onMouseMove = { e => e . stopPropagation ( ) }
418
+ onClick = { ( ) => {
419
+ handleAddSlot ( timeslotTime ) ;
420
+ handlePopupClose ( ) ;
421
+ } }
422
+ disabled = { ! timeslotTime || candidates . includes ( timeslotTime ) }
423
+ >
424
+ < Icon name = "check" onMouseMove = { e => e . stopPropagation ( ) } />
425
+ </ Button >
426
+ </ div >
427
+ }
428
+ />
429
+ </ div >
430
+ </ div >
431
+ }
432
+ />
285
433
) : (
286
434
< div className = { styles [ 'timeline-input-wrapper' ] } >
287
435
< div className = { `${ styles [ 'timeline-input' ] } ${ styles [ 'msg' ] } ` } onClick = { handleStartEditing } >
@@ -316,6 +464,7 @@ function TimelineContent({busySlots: allBusySlots, minHour, maxHour}) {
316
464
} )
317
465
) }
318
466
< TimelineInput minHour = { minHour } maxHour = { maxHour } />
467
+ { /* <TimelineDragAndDrop duration={duration} /> */ }
319
468
</ div >
320
469
) ;
321
470
}
@@ -431,7 +580,7 @@ Timeline.propTypes = {
431
580
} ;
432
581
433
582
Timeline . defaultProps = {
434
- defaultMinHour : 8 ,
583
+ defaultMinHour : 0 ,
435
584
defaultMaxHour : 24 ,
436
585
hourStep : 2 ,
437
586
} ;
0 commit comments