@operato/gantt 8.0.0-alpha.35 → 8.0.0-alpha.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import { TimeScale, Task, TaskTag } from './types'
4
4
  import { OxTaskTooltip } from './ox-task-tooltip'
5
5
  import { drawTimeScaleHeader, getScale, getChartWidth } from './draw-time-scale-header'
6
6
  import { levelDown, levelUp } from './utils'
7
+ import { GestureEventPinch, GestureEventDoubleTap, GestureEventDrag } from '@operato/utils/gesture-mixin.js'
7
8
 
8
9
  export class GanttChartController implements ReactiveController {
9
10
  private host: (ReactiveControllerHost & LitElement) | null
@@ -12,15 +13,6 @@ export class GanttChartController implements ReactiveController {
12
13
  private totalDays: number
13
14
  private _timeScale?: d3.ScaleTime<number, number, never>
14
15
 
15
- private touchStartX: number | null = null
16
- private touchStartY: number | null = null
17
- private scrollStartX: number | null = null
18
- private scrollStartY: number | null = null
19
- private lastTouchDistance?: number | null
20
- private draggingThreshold: number = 20
21
- private dragging: boolean = false
22
- private readyDragging: boolean = false
23
-
24
16
  private colorProvider?: (task: Task) => string
25
17
 
26
18
  public baseWidth: number = 30
@@ -200,17 +192,7 @@ export class GanttChartController implements ReactiveController {
200
192
  .attr('stroke-width', 1)
201
193
 
202
194
  taskBar
203
- .on('click', () => {
204
- this.selectedTaskId = task.id
205
- const customEvent = new CustomEvent('task-clicked', {
206
- detail: { taskId: task.id },
207
- bubbles: true,
208
- composed: true
209
- })
210
- this.host?.dispatchEvent(customEvent)
211
- this.host?.requestUpdate()
212
- })
213
- .on('touchstart', e => {
195
+ .on('click', e => {
214
196
  this.selectedTaskId = task.id
215
197
  const customEvent = new CustomEvent('task-clicked', {
216
198
  detail: { taskId: task.id },
@@ -237,17 +219,7 @@ export class GanttChartController implements ReactiveController {
237
219
  .attr('fill', 'lightgreen')
238
220
 
239
221
  progressBar
240
- .on('click', () => {
241
- this.selectedTaskId = task.id
242
- const customEvent = new CustomEvent('task-clicked', {
243
- detail: { taskId: task.id },
244
- bubbles: true,
245
- composed: true
246
- })
247
- this.host?.dispatchEvent(customEvent)
248
- this.host?.requestUpdate()
249
- })
250
- .on('touchstart', e => {
222
+ .on('click', e => {
251
223
  this.selectedTaskId = task.id
252
224
  const customEvent = new CustomEvent('task-clicked', {
253
225
  detail: { taskId: task.id },
@@ -318,6 +290,8 @@ export class GanttChartController implements ReactiveController {
318
290
  event.preventDefault()
319
291
  event.stopPropagation()
320
292
 
293
+ const { offsetHeight } = container
294
+
321
295
  const deltaModeFactor =
322
296
  event.deltaMode === WheelEvent.DOM_DELTA_LINE
323
297
  ? 16
@@ -329,7 +303,8 @@ export class GanttChartController implements ReactiveController {
329
303
 
330
304
  // 가로 스크롤 및 세로 스크롤 모두 처리
331
305
  container.scrollLeft += deltaX
332
- container.scrollTop += deltaY
306
+ // container.scrollTop += deltaY
307
+ container.scrollTop = Math.min(this.tasks.length * 30 - (offsetHeight - 40), container.scrollTop + deltaY)
333
308
 
334
309
  const customEvent = new CustomEvent('scroll', {
335
310
  detail: {
@@ -343,142 +318,85 @@ export class GanttChartController implements ReactiveController {
343
318
  this.host?.dispatchEvent(customEvent)
344
319
  }
345
320
 
346
- handleTouchStartForBody(event: MouseEvent | TouchEvent, container: HTMLDivElement) {
347
- event.preventDefault()
348
- event.stopPropagation()
349
-
350
- const touch = 'touches' in event
351
- const x = touch ? event.touches[0].clientX : event.clientX
352
- const y = touch ? event.touches[0].clientY : event.clientY
321
+ handlePinchForBody = (event: GestureEventPinch) => {
322
+ const { scale } = event.detail
353
323
 
354
- this.dragging = false
355
- this.readyDragging = true
356
-
357
- if (touch && event.touches.length === 2) {
358
- this.lastTouchDistance = this.getTouchDistance(event.touches)
359
- } else {
360
- this.touchStartX = x
361
- this.touchStartY = y
324
+ this.baseWidth *= scale
362
325
 
363
- this.scrollStartX = container.scrollLeft
364
- this.scrollStartY = container.scrollTop
326
+ if (this.baseWidth > 40) {
327
+ if (this.level === 'week-day') {
328
+ this.baseWidth = 40
329
+ } else {
330
+ this.level = levelDown(this.level)
331
+ this.baseWidth = 30
332
+ }
333
+ } else if (this.baseWidth < 30) {
334
+ if (this.level === 'year-quarter') {
335
+ this.baseWidth = 30
336
+ }
365
337
  }
366
- }
367
338
 
368
- handleTouchMoveForBody(event: MouseEvent | TouchEvent, container: HTMLDivElement) {
369
- if (!this.dragging && !this.readyDragging && !this.lastTouchDistance) {
370
- return
339
+ if (this.baseWidth < 20) {
340
+ this.level = levelUp(this.level)
341
+ this.baseWidth = 30
371
342
  }
372
343
 
373
- event.preventDefault()
374
- event.stopPropagation()
344
+ this.updateTimeScale()
345
+ this.host?.requestUpdate()
375
346
 
376
- const touch = 'touches' in event
377
- const x = touch ? event.touches[0].clientX : event.clientX
378
- const y = touch ? event.touches[0].clientY : event.clientY
379
-
380
- if (touch && event.touches.length > 1 && this.lastTouchDistance !== null) {
381
- const currentDistance = this.getTouchDistance(event.touches)
382
-
383
- const zoomSensitivity = 0.3
384
- let zoomFactor = currentDistance / this.lastTouchDistance!
385
- zoomFactor = 1 + (zoomFactor - 1) * zoomSensitivity
386
-
387
- this.baseWidth *= zoomFactor
388
-
389
- if (this.baseWidth > 40) {
390
- if (this.level === 'week-day') {
391
- this.baseWidth = 40
392
- } else {
393
- this.level = levelDown(this.level)
394
- this.baseWidth = 30
395
- }
396
- } else if (this.baseWidth < 30) {
397
- if (this.level === 'year-quarter') {
398
- this.baseWidth = 30
399
- }
400
- }
347
+ const customEvent = new CustomEvent('timescale-changed', {
348
+ detail: {
349
+ baseWidth: this.baseWidth,
350
+ level: this.level
351
+ },
352
+ bubbles: true,
353
+ composed: true
354
+ })
401
355
 
402
- if (this.baseWidth < 20) {
403
- this.level = levelUp(this.level)
404
- this.baseWidth = 30
405
- }
356
+ this.host?.dispatchEvent(customEvent)
357
+ }
406
358
 
407
- this.lastTouchDistance = currentDistance
408
- this.updateTimeScale()
409
- this.host?.requestUpdate()
410
-
411
- const customEvent = new CustomEvent('timescale-changed', {
412
- detail: {
413
- baseWidth: this.baseWidth,
414
- level: this.level
415
- },
416
- bubbles: true,
417
- composed: true
418
- })
419
- this.host?.dispatchEvent(customEvent)
420
- } else if (this.touchStartX !== null) {
421
- const dx = this.touchStartX! - x
422
- const dy = this.touchStartY! - y
423
-
424
- if (!this.dragging && this.readyDragging) {
425
- if (Math.sqrt(dx * dx + dy * dy) > this.draggingThreshold) {
426
- this.dragging = true
427
- this.readyDragging = false
428
- } else {
429
- return
430
- }
431
- }
359
+ handleDoubleTapForBody = (event: GestureEventDoubleTap) => {
360
+ const container = (this.host as any).container
432
361
 
433
- container.scrollLeft = this.scrollStartX! + dx
434
- container.scrollTop = this.scrollStartY! + dy
362
+ container.scrollLeft = 0
363
+ container.scrollTop = 0
435
364
 
436
- const customEvent = new CustomEvent('scroll', {
437
- detail: {
438
- scrollLeft: container.scrollLeft,
439
- scrollTop: container.scrollTop
440
- },
441
- bubbles: true,
442
- composed: true
443
- })
365
+ this.baseWidth = 30
366
+ this.level = 'week-day'
444
367
 
445
- this.host?.dispatchEvent(customEvent)
446
- }
447
- }
368
+ this.updateTimeScale()
369
+ this.host?.requestUpdate()
448
370
 
449
- handleTouchEndForBody(event: MouseEvent | TouchEvent) {
450
- if (!this.dragging && !this.lastTouchDistance) {
451
- return
452
- }
371
+ const customEvent = new CustomEvent('timescale-changed', {
372
+ detail: {
373
+ baseWidth: this.baseWidth,
374
+ level: this.level
375
+ },
376
+ bubbles: true,
377
+ composed: true
378
+ })
453
379
 
454
- event.preventDefault()
455
- event.stopPropagation()
380
+ this.host?.dispatchEvent(customEvent)
381
+ }
456
382
 
457
- if ('touches' in event && event.touches.length > 1) {
458
- this.lastTouchDistance = null
459
- } else if (this.touchStartX !== null) {
460
- if (!this.dragging) {
461
- this.host?.dispatchEvent(
462
- new CustomEvent('date-range-cleared', {
463
- bubbles: true,
464
- composed: true
465
- })
466
- )
467
- } else {
468
- this.dragging = false
469
- this.readyDragging = false
470
- }
383
+ handleDragForBody = (event: GestureEventDrag) => {
384
+ const { deltaX: dx, deltaY: dy } = event.detail
385
+ const container = (this.host as any).container
386
+ const { offsetHeight } = container
471
387
 
472
- this.touchStartX = null
473
- this.touchStartY = null
474
- this.scrollStartX = null
475
- this.scrollStartY = null
476
- }
477
- }
388
+ container.scrollLeft -= dx
389
+ container.scrollTop = Math.min(this.tasks.length * 30 - (offsetHeight - 40), container.scrollTop - dy)
478
390
 
479
- getTouchDistance(touches: TouchList): number {
480
- const dx = touches[0].clientX - touches[1].clientX
481
- const dy = touches[0].clientY - touches[1].clientY
482
- return Math.sqrt(dx * dx + dy * dy)
391
+ const customEvent = new CustomEvent('scroll', {
392
+ detail: {
393
+ scrollLeft: container.scrollLeft,
394
+ scrollTop: container.scrollTop
395
+ },
396
+ bubbles: true,
397
+ composed: true
398
+ })
399
+
400
+ this.host?.dispatchEvent(customEvent)
483
401
  }
484
402
  }
@@ -13,8 +13,10 @@ import { Task, TimeScale } from './types.js'
13
13
  import { levelDown, levelUp } from './utils'
14
14
  import { OxTaskTooltip } from './ox-task-tooltip.js'
15
15
 
16
+ import { gesture } from '@operato/utils/gesture-mixin.js'
17
+
16
18
  @customElement('ox-gantt-chart')
17
- export class OxGanttChart extends LitElement {
19
+ export class OxGanttChart extends gesture(LitElement) {
18
20
  static styles = [
19
21
  MDTypeScaleStyles,
20
22
  ScrollbarStyles,
@@ -31,6 +33,7 @@ export class OxGanttChart extends LitElement {
31
33
  overflow-x: auto;
32
34
  overflow-y: hidden;
33
35
  overscroll-behavior: none;
36
+ touch-action: none;
34
37
  }
35
38
 
36
39
  svg {
@@ -91,12 +94,7 @@ export class OxGanttChart extends LitElement {
91
94
  @query('[chart-container]') container!: HTMLDivElement
92
95
  @query('ox-task-tooltip') tooltip!: OxTaskTooltip
93
96
 
94
- private ganttChartController: GanttChartController
95
-
96
- constructor() {
97
- super()
98
- this.ganttChartController = new GanttChartController(this, this.fromDate, this.toDate, this.axisScale)
99
- }
97
+ private ganttChartController!: GanttChartController
100
98
 
101
99
  protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
102
100
  this.initHeaderGestureHandlers()
@@ -104,9 +102,22 @@ export class OxGanttChart extends LitElement {
104
102
  this.addEventListener('date-range-cleared', e => this.clearSelection())
105
103
  }
106
104
 
105
+ connectedCallback(): void {
106
+ super.connectedCallback()
107
+ this.ganttChartController = new GanttChartController(this, this.fromDate, this.toDate, this.axisScale)
108
+
109
+ this.addEventListener('pinch', this.ganttChartController.handlePinchForBody as EventListener)
110
+ this.addEventListener('drag', this.ganttChartController.handleDragForBody as EventListener)
111
+ this.addEventListener('doubletap', this.ganttChartController.handleDoubleTapForBody as EventListener)
112
+ }
113
+
107
114
  disconnectedCallback(): void {
108
115
  super.disconnectedCallback()
109
116
 
117
+ this.removeEventListener('pinch', this.ganttChartController.handlePinchForBody as EventListener)
118
+ this.removeEventListener('drag', this.ganttChartController.handleDragForBody as EventListener)
119
+ this.removeEventListener('doubletap', this.ganttChartController.handleDoubleTapForBody as EventListener)
120
+
110
121
  this.ganttChartController.dispose()
111
122
  }
112
123
 
@@ -116,8 +127,9 @@ export class OxGanttChart extends LitElement {
116
127
  }
117
128
 
118
129
  if (changedProperties.has('tasks')) {
119
- this.ganttChartController.setTasks(this.flattenTasks(this.tasks))
120
- this.svgBody.style.height = Math.max(this.tasks.length * 30, 2000) + 'px'
130
+ const flattenTasks = this.flattenTasks(this.tasks)
131
+ this.ganttChartController.setTasks(flattenTasks)
132
+ this.svgBody.style.height = Math.max(flattenTasks.length * 30, 2000) + 'px'
121
133
  }
122
134
 
123
135
  if (changedProperties.has('locale')) {
@@ -143,16 +155,7 @@ export class OxGanttChart extends LitElement {
143
155
 
144
156
  render() {
145
157
  return html`
146
- <div
147
- chart-container
148
- @wheel=${(e: WheelEvent) => this.ganttChartController.handleWheelForBody(e, this.container)}
149
- @mousedown=${(e: MouseEvent) => this.ganttChartController.handleTouchStartForBody(e, this.container)}
150
- @mousemove=${(e: MouseEvent) => this.ganttChartController.handleTouchMoveForBody(e, this.container)}
151
- @mouseup=${(e: MouseEvent) => this.ganttChartController.handleTouchEndForBody(e)}
152
- @touchstart=${(e: TouchEvent) => this.ganttChartController.handleTouchStartForBody(e, this.container)}
153
- @touchmove=${(e: TouchEvent) => this.ganttChartController.handleTouchMoveForBody(e, this.container)}
154
- @touchend=${(e: TouchEvent) => this.ganttChartController.handleTouchEndForBody(e)}
155
- >
158
+ <div chart-container @wheel=${(e: WheelEvent) => this.ganttChartController.handleWheelForBody(e, this.container)}>
156
159
  <svg
157
160
  header
158
161
  @date-range-selected=${(e: CustomEvent) => this.handleDateRangeSelected(e)}
@@ -279,88 +282,44 @@ export class OxGanttChart extends LitElement {
279
282
  initHeaderGestureHandlers() {
280
283
  const header = d3.select(this.svgHeader)
281
284
 
282
- let isDragging = false
283
285
  let startX: number | null = null
284
286
 
285
- const handleTouchStart = (event: TouchEvent) => {
287
+ const handlePointerDown = (event: PointerEvent) => {
286
288
  event.preventDefault()
287
289
  event.stopPropagation()
288
290
 
289
- isDragging = true
290
- const touch = event.touches[0]
291
291
  const containerOffsetX = header.node()!.getBoundingClientRect().left
292
- startX = touch.clientX - containerOffsetX
293
- }
294
-
295
- const handleTouchMove = (event: TouchEvent) => {
296
- event.preventDefault()
297
- event.stopPropagation()
292
+ startX = event.clientX - containerOffsetX
298
293
 
299
- if (isDragging && startX !== null) {
300
- const touch = event.touches[0]
301
- const containerOffsetX = header.node()!.getBoundingClientRect().left
302
- const currentX = touch.clientX - containerOffsetX
303
- this.highlightSelection(startX, currentX)
304
- }
294
+ window.addEventListener('pointermove', handlePointerMove)
295
+ window.addEventListener('pointerup', handlePointerUp)
305
296
  }
306
297
 
307
- const handleTouchEnd = (event: TouchEvent) => {
298
+ const handlePointerMove = (event: PointerEvent) => {
308
299
  event.preventDefault()
309
300
  event.stopPropagation()
310
301
 
311
- if (!isDragging || startX === null) {
312
- const touch = event.changedTouches[0]
313
- const containerOffsetX = header.node()!.getBoundingClientRect().left
314
- const clickX = touch.clientX - containerOffsetX
315
- this.finalizeSelection(clickX, clickX)
316
- return
317
- }
318
-
319
- const touch = event.changedTouches[0]
320
302
  const containerOffsetX = header.node()!.getBoundingClientRect().left
321
- const endX = touch.clientX - containerOffsetX
322
- this.finalizeSelection(startX, endX)
323
-
324
- isDragging = false
325
- startX = null
326
- }
303
+ const currentX = event.clientX - containerOffsetX
327
304
 
328
- const handleMouseDown = (event: MouseEvent) => {
329
- isDragging = true
330
- const containerOffsetX = header.node()!.getBoundingClientRect().left
331
- startX = event.clientX - containerOffsetX
305
+ this.highlightSelection(startX!, currentX)
332
306
  }
333
307
 
334
- const handleMouseMove = (event: MouseEvent) => {
335
- if (!isDragging || startX === null) return
336
-
337
- const containerOffsetX = header.node()!.getBoundingClientRect().left
338
- const currentX = event.clientX - containerOffsetX
339
- this.highlightSelection(startX, currentX)
340
- }
308
+ const handlePointerUp = (event: PointerEvent) => {
309
+ event.preventDefault()
310
+ event.stopPropagation()
341
311
 
342
- const handleMouseUp = (event: MouseEvent) => {
343
- if (!isDragging || startX === null) {
344
- const containerOffsetX = header.node()!.getBoundingClientRect().left
345
- const clickX = event.clientX - containerOffsetX
346
- this.finalizeSelection(clickX, clickX)
347
- return
348
- }
312
+ window.removeEventListener('pointermove', handlePointerMove)
313
+ window.removeEventListener('pointerup', handlePointerUp)
349
314
 
350
315
  const containerOffsetX = header.node()!.getBoundingClientRect().left
351
316
  const endX = event.clientX - containerOffsetX
352
- this.finalizeSelection(startX, endX)
317
+ this.finalizeSelection(startX!, endX)
353
318
 
354
- isDragging = false
355
319
  startX = null
356
320
  }
357
321
 
358
- header.on('mousedown', handleMouseDown)
359
- header.on('mousemove', handleMouseMove)
360
- header.on('mouseup', handleMouseUp)
361
- header.on('touchstart', handleTouchStart)
362
- header.on('touchmove', handleTouchMove)
363
- header.on('touchend', handleTouchEnd)
322
+ header.on('pointerdown', handlePointerDown)
364
323
  }
365
324
 
366
325
  highlightSelection(startX: number, endX: number) {
@@ -95,7 +95,7 @@ const Template: Story<ArgTypes> = ({
95
95
 
96
96
  ox-gantt {
97
97
  width: 95%;
98
- height: 650px;
98
+ height: 800px;
99
99
  box-sizing: border-box;
100
100
 
101
101
  background-color: var(--md-sys-color-primary-container);